diff --git a/alias.go b/alias.go index 3a09919..3bacc67 100644 --- a/alias.go +++ b/alias.go @@ -31,3 +31,34 @@ type Box = chart.Box type Renderer = chart.Renderer type Style = chart.Style type Color = drawing.Color + +type Point struct { + X int + Y int +} + +const ( + ChartTypeLine = "line" + ChartTypeBar = "bar" + ChartTypePie = "pie" + ChartTypeRadar = "radar" + ChartTypeFunnel = "funnel" +) + +const ( + ChartOutputSVG = "svg" + ChartOutputPNG = "png" +) + +const ( + PositionLeft = "left" + PositionRight = "right" + PositionCenter = "center" + PositionTop = "top" + PositionBottom = "bottom" +) + +const ( + OrientHorizontal = "horizontal" + OrientVertical = "vertical" +) diff --git a/axis.go b/axis.go deleted file mode 100644 index 5881f5e..0000000 --- a/axis.go +++ /dev/null @@ -1,472 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "math" - - "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" -) - -type AxisOption struct { - // The boundary gap on both sides of a coordinate axis. - // Nil or *true means the center part of two axis ticks - BoundaryGap *bool - // The flag for show axis, set this to *false will hide axis - Show *bool - // The position of axis, it can be 'left', 'top', 'right' or 'bottom' - Position string - // Number of segments that the axis is split into. Note that this number serves only as a recommendation. - SplitNumber int - ClassName string - // The line color of axis - StrokeColor Color - // The line width - StrokeWidth float64 - // The length of the axis tick - TickLength int - // The flag for show axis tick, set this to *false will hide axis tick - TickShow *bool - // The margin value of label - LabelMargin int - // The font size of label - FontSize float64 - // The font of label - Font *truetype.Font - // The color of label - FontColor Color - // The flag for show axis split line, set this to true will show axis split line - SplitLineShow bool - // The color of split line - SplitLineColor Color -} - -type axis struct { - painter *Painter - data *AxisDataList - option *AxisOption -} -type axisMeasurement struct { - Width int - Height int -} - -// NewAxis creates a new axis with data and style options -func NewAxis(p *Painter, data AxisDataList, option AxisOption) *axis { - return &axis{ - painter: p, - data: &data, - option: &option, - } - -} - -// GetLabelMargin returns the label margin value -func (as *AxisOption) GetLabelMargin() int { - return getDefaultInt(as.LabelMargin, 8) -} - -// GetTickLength returns the tick length value -func (as *AxisOption) GetTickLength() int { - return getDefaultInt(as.TickLength, 5) -} - -// Style returns the style of axis -func (as *AxisOption) Style(f *truetype.Font) chart.Style { - s := chart.Style{ - ClassName: as.ClassName, - StrokeColor: as.StrokeColor, - StrokeWidth: as.StrokeWidth, - FontSize: as.FontSize, - FontColor: as.FontColor, - Font: as.Font, - } - if s.FontSize == 0 { - s.FontSize = chart.DefaultFontSize - } - if s.Font == nil { - s.Font = f - } - return s -} - -type AxisData struct { - // The text value of axis - Text string -} -type AxisDataList []AxisData - -// TextList returns the text list of axis data -func (l AxisDataList) TextList() []string { - textList := make([]string, len(l)) - for index, item := range l { - textList[index] = item.Text - } - return textList -} - -type axisRenderOption struct { - textMaxWith int - textMaxHeight int - boundaryGap bool - unitCount int - modValue int -} - -// NewAxisDataListFromStringList creates a new axis data list from string list -func NewAxisDataListFromStringList(textList []string) AxisDataList { - list := make(AxisDataList, len(textList)) - for index, text := range textList { - list[index] = AxisData{ - Text: text, - } - } - return list -} - -func (a *axis) axisLabel(renderOpt *axisRenderOption) { - option := a.option - data := *a.data - // d := a.d - if option.FontColor.IsZero() || len(data) == 0 { - return - } - // r := d.Render - - // s.GetTextOptions().WriteTextOptionsToRenderer(r) - p := a.painter - s := option.Style(p.font) - p.SetTextStyle(s) - - width := p.Width() - height := p.Height() - textList := data.TextList() - count := len(textList) - - boundaryGap := renderOpt.boundaryGap - if !boundaryGap { - count-- - } - - unitCount := renderOpt.unitCount - modValue := renderOpt.modValue - labelMargin := option.GetLabelMargin() - - // 轴线 - labelHeight := labelMargin + renderOpt.textMaxHeight - labelWidth := labelMargin + renderOpt.textMaxWith - - // 坐标轴文本 - position := option.Position - switch position { - case PositionLeft: - fallthrough - case PositionRight: - values := autoDivide(height, count) - textList := data.TextList() - // 由下往上 - reverseIntSlice(values) - for index, text := range textList { - y := values[index] - 2 - b := p.MeasureText(text) - if boundaryGap { - height := y - values[index+1] - y -= (height - b.Height()) >> 1 - } else { - y += b.Height() >> 1 - } - // 左右位置的x不一样 - x := width - renderOpt.textMaxWith - if position == PositionLeft { - x = labelWidth - b.Width() - 1 - } - p.Text(text, x, y) - } - default: - // 定位bottom,重新计算y0的定位 - y0 := height - labelMargin - if position == PositionTop { - y0 = labelHeight - labelMargin - } - values := autoDivide(width, count) - for index, text := range data.TextList() { - if unitCount != 0 && index%unitCount != modValue { - continue - } - x := values[index] - leftOffset := 0 - b := p.MeasureText(text) - if boundaryGap { - width := values[index+1] - x - leftOffset = (width - b.Width()) >> 1 - } else { - // 左移文本长度 - leftOffset = -b.Width() >> 1 - } - p.Text(text, x+leftOffset, y0) - } - } -} - -func (a *axis) axisLine(renderOpt *axisRenderOption) { - // d := a.d - // r := d.Render - p := a.painter - option := a.option - s := option.Style(p.font) - p.SetDrawingStyle(s.GetStrokeOptions()) - - x0 := 0 - y0 := 0 - x1 := 0 - y1 := 0 - width := p.Width() - height := p.Height() - labelMargin := option.GetLabelMargin() - - // 轴线 - labelHeight := labelMargin + renderOpt.textMaxHeight - labelWidth := labelMargin + renderOpt.textMaxWith - tickLength := option.GetTickLength() - switch option.Position { - case PositionLeft: - x0 = tickLength + labelWidth - x1 = x0 - y0 = 0 - y1 = height - case PositionRight: - x0 = width - labelWidth - x1 = x0 - y0 = 0 - y1 = height - case PositionTop: - x0 = 0 - x1 = width - y0 = labelHeight - y1 = y0 - // bottom - default: - x0 = 0 - x1 = width - y0 = height - tickLength - labelHeight - y1 = y0 - } - - p.MoveTo(x0, y0) - p.LineTo(x1, y1) - p.FillStroke() -} - -func (a *axis) axisTick(renderOpt *axisRenderOption) { - // d := a.d - // r := d.Render - - p := a.painter - option := a.option - s := option.Style(p.font) - p.SetDrawingStyle(s.GetStrokeOptions()) - - width := p.Width() - height := p.Height() - data := *a.data - tickCount := len(data) - if tickCount == 0 { - return - } - if !renderOpt.boundaryGap { - tickCount-- - } - labelMargin := option.GetLabelMargin() - tickShow := true - if isFalse(option.TickShow) { - tickShow = false - } - unitCount := renderOpt.unitCount - - tickLengthValue := option.GetTickLength() - labelHeight := labelMargin + renderOpt.textMaxHeight - labelWidth := labelMargin + renderOpt.textMaxWith - position := option.Position - switch position { - case PositionLeft: - fallthrough - case PositionRight: - values := autoDivide(height, tickCount) - // 左右仅是x0的位置不一样 - x0 := width - labelWidth - if option.Position == PositionLeft { - x0 = labelWidth - } - if tickShow { - for _, v := range values { - x := x0 - y := v - p.MoveTo(x, y) - p.LineTo(x+tickLengthValue, y) - p.Stroke() - } - } - // 辅助线 - if option.SplitLineShow && !option.SplitLineColor.IsZero() { - p.SetStrokeColor(option.SplitLineColor) - splitLineWidth := width - labelWidth - tickLengthValue - x0 = labelWidth + tickLengthValue - if position == PositionRight { - x0 = 0 - splitLineWidth = width - labelWidth - 1 - } - for _, v := range values[0 : len(values)-1] { - x := x0 - y := v - p.MoveTo(x, y) - p.LineTo(x+splitLineWidth, y) - p.Stroke() - } - } - default: - values := autoDivide(width, tickCount) - // 上下仅是y0的位置不一样 - y0 := height - labelHeight - if position == PositionTop { - y0 = labelHeight - } - if tickShow { - for index, v := range values { - if index%unitCount != 0 { - continue - } - x := v - y := y0 - p.MoveTo(x, y-tickLengthValue) - p.LineTo(x, y) - p.Stroke() - } - } - // 辅助线 - if option.SplitLineShow && !option.SplitLineColor.IsZero() { - p.SetStrokeColor(option.SplitLineColor) - y0 = 0 - splitLineHeight := height - labelHeight - tickLengthValue - if position == PositionTop { - y0 = labelHeight - splitLineHeight = height - labelHeight - } - - for index, v := range values { - if index%unitCount != 0 { - continue - } - x := v - y := y0 - - p.MoveTo(x, y) - p.LineTo(x, y0+splitLineHeight) - p.Stroke() - } - } - } -} - -func (a *axis) measureTextMaxWidthHeight() (int, int) { - // d := a.d - // r := d.Render - p := a.painter - s := a.option.Style(p.font) - data := a.data - p.SetDrawingStyle(s.GetStrokeOptions()) - p.SetTextStyle(s.GetTextOptions()) - return measureTextMaxWidthHeight(data.TextList(), p) -} - -// measure returns the measurement of axis. -// Width will be textMaxWidth + labelMargin + tickLength for position left or right. -// Height will be textMaxHeight + labelMargin + tickLength for position top or bottom. -func (a *axis) measure() axisMeasurement { - option := a.option - value := option.GetLabelMargin() + option.GetTickLength() - textMaxWidth, textMaxHeight := a.measureTextMaxWidthHeight() - info := axisMeasurement{} - if option.Position == PositionLeft || - option.Position == PositionRight { - info.Width = textMaxWidth + value - } else { - info.Height = textMaxHeight + value - } - return info -} - -// Render renders the axis for chart -func (a *axis) Render() { - option := a.option - if isFalse(option.Show) { - return - } - textMaxWidth, textMaxHeight := a.measureTextMaxWidthHeight() - opt := &axisRenderOption{ - textMaxWith: textMaxWidth, - textMaxHeight: textMaxHeight, - boundaryGap: true, - } - if isFalse(option.BoundaryGap) { - opt.boundaryGap = false - } - - unitCount := chart.MaxInt(option.SplitNumber, 1) - width := a.painter.Width() - textList := a.data.TextList() - count := len(textList) - - position := option.Position - switch position { - case PositionLeft: - fallthrough - case PositionRight: - default: - maxCount := width / (opt.textMaxWith + 10) - // 可以显示所有 - if maxCount >= count { - unitCount = 1 - } else if maxCount < count/unitCount { - unitCount = int(math.Ceil(float64(count) / float64(maxCount))) - } - } - - boundaryGap := opt.boundaryGap - modValue := 0 - if boundaryGap && unitCount > 1 { - // 如果是居中,unit count需要设置为奇数 - if unitCount%2 == 0 { - unitCount++ - } - modValue = unitCount / 2 - } - opt.modValue = modValue - opt.unitCount = unitCount - - // 坐标轴线 - a.axisLine(opt) - a.axisTick(opt) - // 坐标文本 - a.axisLabel(opt) -} diff --git a/axis_test.go b/axis_test.go deleted file mode 100644 index fe253e9..0000000 --- a/axis_test.go +++ /dev/null @@ -1,259 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "testing" - - "github.com/golang/freetype/truetype" - "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -func TestAxisOption(t *testing.T) { - assert := assert.New(t) - - as := AxisOption{} - - assert.Equal(8, as.GetLabelMargin()) - as.LabelMargin = 10 - assert.Equal(10, as.GetLabelMargin()) - - assert.Equal(5, as.GetTickLength()) - as.TickLength = 6 - assert.Equal(6, as.GetTickLength()) - - f := &truetype.Font{} - style := as.Style(f) - assert.Equal(float64(10), style.FontSize) - assert.Equal(f, style.Font) -} - -func TestAxisDataList(t *testing.T) { - assert := assert.New(t) - - textList := []string{ - "a", - "b", - } - data := NewAxisDataListFromStringList(textList) - assert.Equal(textList, data.TextList()) -} - -func TestAxis(t *testing.T) { - assert := assert.New(t) - - axisData := NewAxisDataListFromStringList([]string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }) - getDefaultOption := func() AxisOption { - return AxisOption{ - StrokeColor: drawing.ColorBlack, - StrokeWidth: 1, - FontColor: drawing.ColorBlack, - Show: TrueFlag(), - TickShow: TrueFlag(), - SplitLineShow: true, - SplitLineColor: drawing.ColorBlack.WithAlpha(60), - } - } - tests := []struct { - newOption func() AxisOption - newData func() AxisDataList - result string - }{ - // 文本按起始位置展示 - // axis位于bottom - { - newOption: func() AxisOption { - opt := getDefaultOption() - opt.BoundaryGap = FalseFlag() - return opt - }, - result: "\\nMonTueWedThuFriSatSun", - }, - // 文本居中展示 - // axis位于bottom - { - newOption: func() AxisOption { - opt := getDefaultOption() - return opt - }, - result: "\\nMonTueWedThuFriSatSun", - }, - // 文本按起始位置展示 - // axis位于top - { - newOption: func() AxisOption { - opt := getDefaultOption() - opt.Position = PositionTop - opt.BoundaryGap = FalseFlag() - return opt - }, - result: "\\nMonTueWedThuFriSatSun", - }, - // 文本居中展示 - // axis位于top - { - newOption: func() AxisOption { - opt := getDefaultOption() - opt.Position = PositionTop - return opt - }, - result: "\\nMonTueWedThuFriSatSun", - }, - // 文本按起始位置展示 - // axis位于left - { - newOption: func() AxisOption { - opt := getDefaultOption() - opt.Position = PositionLeft - opt.BoundaryGap = FalseFlag() - return opt - }, - result: "\\nMonTueWedThuFriSatSun", - }, - // 文本居中展示 - // axis位于left - { - newOption: func() AxisOption { - opt := getDefaultOption() - opt.Position = PositionLeft - return opt - }, - result: "\\nMonTueWedThuFriSatSun", - }, - // 文本按起始位置展示 - // axis位于right - { - newOption: func() AxisOption { - opt := getDefaultOption() - opt.Position = PositionRight - opt.BoundaryGap = FalseFlag() - return opt - }, - result: "\\nMonTueWedThuFriSatSun", - }, - // 文本居中展示 - // axis位于right - { - newOption: func() AxisOption { - opt := getDefaultOption() - opt.Position = PositionRight - return opt - }, - result: "\\nMonTueWedThuFriSatSun", - }, - // text较多,仅展示部分 - { - newOption: func() AxisOption { - opt := getDefaultOption() - opt.Position = PositionBottom - return opt - }, - newData: func() AxisDataList { - return NewAxisDataListFromStringList([]string{ - "01-01", - "01-02", - "01-03", - "01-04", - "01-05", - "01-06", - "01-07", - "01-08", - "01-09", - "01-10", - "01-11", - "01-12", - "01-13", - "01-14", - "01-15", - "01-16", - "01-17", - "01-18", - "01-19", - "01-20", - "01-21", - }) - }, - result: "\\n01-0201-0501-0801-1101-1401-1701-20", - }, - } - for _, tt := range tests { - p, err := NewPainter(PainterOptions{ - Width: 400, - Height: 300, - }, PainterPaddingOption(chart.Box{ - Left: 5, - Top: 5, - Right: 5, - Bottom: 5, - })) - assert.Nil(err) - style := tt.newOption() - data := axisData - if tt.newData != nil { - data = tt.newData() - } - NewAxis(p, data, style).Render() - - result, err := p.Bytes() - assert.Nil(err) - assert.Equal(tt.result, string(result)) - } -} - -func TestMeasureAxis(t *testing.T) { - assert := assert.New(t) - - p, err := NewPainter(PainterOptions{ - Width: 400, - Height: 300, - }) - assert.Nil(err) - data := NewAxisDataListFromStringList([]string{ - "Mon", - "Sun", - }) - f, _ := chart.GetDefaultFont() - width := NewAxis(p, data, AxisOption{ - FontSize: 12, - Font: f, - Position: PositionLeft, - }).measure().Width - assert.Equal(44, width) - - height := NewAxis(p, data, AxisOption{ - FontSize: 12, - Font: f, - Position: PositionTop, - }).measure().Height - assert.Equal(28, height) -} diff --git a/bar.go b/bar.go deleted file mode 100644 index 1090f6b..0000000 --- a/bar.go +++ /dev/null @@ -1,58 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -type BarStyle struct { - ClassName string - StrokeDashArray []float64 - FillColor drawing.Color -} - -func (bs *BarStyle) Style() chart.Style { - return chart.Style{ - ClassName: bs.ClassName, - StrokeDashArray: bs.StrokeDashArray, - StrokeColor: bs.FillColor, - StrokeWidth: 1, - FillColor: bs.FillColor, - } -} - -// Bar renders bar for chart -func (d *Draw) Bar(b chart.Box, style BarStyle) { - s := style.Style() - - r := d.Render - s.GetFillAndStrokeOptions().WriteToRenderer(r) - d.moveTo(b.Left, b.Top) - d.lineTo(b.Right, b.Top) - d.lineTo(b.Right, b.Bottom) - d.lineTo(b.Left, b.Bottom) - d.lineTo(b.Left, b.Top) - d.Render.FillStroke() -} diff --git a/bar_chart.go b/bar_chart.go deleted file mode 100644 index 32373b1..0000000 --- a/bar_chart.go +++ /dev/null @@ -1,163 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" -) - -type barChartOption struct { - // The series list fo bar chart - SeriesList SeriesList - // The theme - Theme string - // The font - Font *truetype.Font -} - -func barChartRender(opt barChartOption, result *basicRenderResult) ([]markPointRenderOption, error) { - d, err := NewDraw(DrawOption{ - Parent: result.d, - }, PaddingOption(chart.Box{ - Top: result.titleBox.Height(), - // TODO 后续考虑是否需要根据左侧是否展示y轴再生成对应的left - Left: YAxisWidth, - })) - if err != nil { - return nil, err - } - xRange := result.xRange - x0, x1 := xRange.GetRange(0) - width := int(x1 - x0) - // 每一块之间的margin - margin := 10 - // 每一个bar之间的margin - barMargin := 5 - if width < 20 { - margin = 2 - barMargin = 2 - } else if width < 50 { - margin = 5 - barMargin = 3 - } - - seriesCount := len(opt.SeriesList) - // 总的宽度-两个margin-(总数-1)的barMargin - barWidth := (width - 2*margin - barMargin*(seriesCount-1)) / len(opt.SeriesList) - - barMaxHeight := result.getYRange(0).Size - theme := NewTheme(opt.Theme) - - seriesNames := opt.SeriesList.Names() - - r := d.Render - - markPointRenderOptions := make([]markPointRenderOption, 0) - - for i, s := range opt.SeriesList { - // 由于series是for range,为同一个数据,因此需要clone - // 后续需要使用,如mark point - series := s - yRange := result.getYRange(series.YAxisIndex) - points := make([]Point, len(series.Data)) - index := series.index - if index == 0 { - index = i - } - seriesColor := theme.GetSeriesColor(index) - // mark line - markLineRender(markLineRenderOption{ - Draw: d, - FillColor: seriesColor, - FontColor: theme.GetTextColor(), - StrokeColor: seriesColor, - Font: opt.Font, - Series: &series, - Range: yRange, - }) - divideValues := xRange.AutoDivide() - for j, item := range series.Data { - if j >= xRange.divideCount { - continue - } - x := divideValues[j] - x += margin - if i != 0 { - x += i * (barWidth + barMargin) - } - - h := int(yRange.getHeight(item.Value)) - fillColor := seriesColor - if !item.Style.FillColor.IsZero() { - fillColor = item.Style.FillColor - } - top := barMaxHeight - h - d.Bar(chart.Box{ - Top: top, - Left: x, - Right: x + barWidth, - Bottom: barMaxHeight - 1, - }, BarStyle{ - FillColor: fillColor, - }) - // 用于生成marker point - points[j] = Point{ - // 居中的位置 - X: x + barWidth>>1, - Y: top, - } - // 如果label不需要展示,则返回 - if !series.Label.Show { - continue - } - distance := series.Label.Distance - if distance == 0 { - distance = 5 - } - text := NewValueLabelFormater(seriesNames, series.Label.Formatter)(i, item.Value, -1) - labelStyle := chart.Style{ - FontColor: theme.GetTextColor(), - FontSize: labelFontSize, - Font: opt.Font, - } - if !series.Label.Color.IsZero() { - labelStyle.FontColor = series.Label.Color - } - labelStyle.GetTextOptions().WriteToRenderer(r) - textBox := r.MeasureText(text) - d.text(text, x+(barWidth-textBox.Width())>>1, barMaxHeight-h-distance) - } - - // 生成mark point的参数 - markPointRenderOptions = append(markPointRenderOptions, markPointRenderOption{ - Draw: d, - FillColor: seriesColor, - Font: opt.Font, - Points: points, - Series: &series, - }) - } - - return markPointRenderOptions, nil -} diff --git a/bar_chart_test.go b/bar_chart_test.go deleted file mode 100644 index f10a1bc..0000000 --- a/bar_chart_test.go +++ /dev/null @@ -1,131 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -func TestBarChartRender(t *testing.T) { - assert := assert.New(t) - - width := 400 - height := 300 - d, err := NewDraw(DrawOption{ - Width: width, - Height: height, - }) - assert.Nil(err) - - result := basicRenderResult{ - xRange: &Range{ - Min: 0, - Max: 4, - divideCount: 4, - Size: width, - Boundary: true, - }, - yRangeList: []*Range{ - { - divideCount: 6, - Max: 100, - Min: 0, - Size: height, - }, - }, - d: d, - } - f, _ := chart.GetDefaultFont() - - markPointOptions, err := barChartRender(barChartOption{ - Font: f, - SeriesList: SeriesList{ - { - Label: SeriesLabel{ - Show: true, - Color: drawing.ColorBlue, - }, - MarkLine: NewMarkLine( - SeriesMarkDataTypeMin, - ), - Data: []SeriesData{ - { - Value: 20, - }, - { - Value: 60, - Style: chart.Style{ - FillColor: drawing.ColorRed, - }, - }, - { - Value: 90, - }, - }, - }, - NewSeriesFromValues([]float64{ - 80, - 30, - 70, - }), - }, - }, &result) - assert.Nil(err) - assert.Equal(2, len(markPointOptions)) - assert.Equal([]Point{ - { - X: 28, - Y: 240, - }, - { - X: 128, - Y: 120, - }, - { - X: 228, - Y: 30, - }, - }, markPointOptions[0].Points) - assert.Equal([]Point{ - { - X: 70, - Y: 60, - }, - { - X: 170, - Y: 210, - }, - { - X: 270, - Y: 90, - }, - }, markPointOptions[1].Points) - - data, err := d.Bytes() - assert.Nil(err) - assert.Equal("\\n20206090", string(data)) -} diff --git a/bar_test.go b/bar_test.go deleted file mode 100644 index 01b6d3c..0000000 --- a/bar_test.go +++ /dev/null @@ -1,78 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -func TestBarStyle(t *testing.T) { - assert := assert.New(t) - - bs := BarStyle{ - ClassName: "test", - StrokeDashArray: []float64{ - 1.0, - }, - FillColor: drawing.ColorBlack, - } - - assert.Equal(chart.Style{ - ClassName: "test", - StrokeDashArray: []float64{ - 1.0, - }, - StrokeWidth: 1, - FillColor: drawing.ColorBlack, - StrokeColor: drawing.ColorBlack, - }, bs.Style()) -} - -func TestDrawBar(t *testing.T) { - assert := assert.New(t) - d, err := NewDraw(DrawOption{ - Width: 400, - Height: 300, - }, PaddingOption(chart.Box{ - Left: 10, - Top: 20, - Right: 30, - Bottom: 40, - })) - assert.Nil(err) - d.Bar(chart.Box{ - Left: 0, - Top: 0, - Right: 20, - Bottom: 200, - }, BarStyle{ - FillColor: drawing.ColorBlack, - }) - data, err := d.Bytes() - assert.Nil(err) - assert.Equal("\\n", string(data)) -} diff --git a/chart.go b/chart.go deleted file mode 100644 index 21f2071..0000000 --- a/chart.go +++ /dev/null @@ -1,502 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "errors" - "math" - "sort" - "strings" - - "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -const ( - ChartTypeLine = "line" - ChartTypeBar = "bar" - ChartTypePie = "pie" - ChartTypeRadar = "radar" - ChartTypeFunnel = "funnel" -) - -const ( - ChartOutputSVG = "svg" - ChartOutputPNG = "png" -) - -type Point struct { - X int - Y int -} - -const labelFontSize = 10 -const defaultDotWidth = 2.0 -const defaultStrokeWidth = 2.0 - -var defaultChartWidth = 600 -var defaultChartHeight = 400 - -type ChartOption struct { - // The output type of chart, "svg" or "png", default value is "svg" - Type string - // The font family, which should be installed first - FontFamily string - // The font of chart, the default font is "roboto" - Font *truetype.Font - // The theme of chart, "light" and "dark". - // The default theme is "light" - Theme string - // The title option - Title TitleOption - // The legend option - Legend LegendOption - // The x axis option - XAxis XAxisOption - // The y axis option list - YAxisList []YAxisOption - // The width of chart, default width is 600 - Width int - // The height of chart, default height is 400 - Height int - Parent *Draw - // The padding for chart, default padding is [20, 10, 10, 10] - Padding chart.Box - // The canvas box for chart - Box chart.Box - // The series list - SeriesList SeriesList - // The radar indicator list - RadarIndicators []RadarIndicator - // The background color of chart - BackgroundColor drawing.Color - // The child charts - Children []ChartOption -} - -// FillDefault fills the default value for chart option -func (o *ChartOption) FillDefault(theme string) { - t := NewTheme(theme) - // 如果为空,初始化 - yAxisCount := 1 - for _, series := range o.SeriesList { - if series.YAxisIndex >= yAxisCount { - yAxisCount++ - } - } - yAxisList := make([]YAxisOption, yAxisCount) - copy(yAxisList, o.YAxisList) - o.YAxisList = yAxisList - - if o.Font == nil { - o.Font, _ = chart.GetDefaultFont() - } - if o.BackgroundColor.IsZero() { - o.BackgroundColor = t.GetBackgroundColor() - } - if o.Padding.IsZero() { - o.Padding = chart.Box{ - Top: 10, - Right: 10, - Bottom: 10, - Left: 10, - } - } - - // 标题的默认值 - if o.Title.Style.FontColor.IsZero() { - o.Title.Style.FontColor = t.GetTextColor() - } - if o.Title.Style.FontSize == 0 { - o.Title.Style.FontSize = 14 - } - if o.Title.Style.Font == nil { - o.Title.Style.Font = o.Font - } - if o.Title.Style.Padding.IsZero() { - o.Title.Style.Padding = chart.Box{ - Bottom: 10, - } - } - // 副标题 - if o.Title.SubtextStyle.FontColor.IsZero() { - o.Title.SubtextStyle.FontColor = o.Title.Style.FontColor.WithAlpha(180) - } - if o.Title.SubtextStyle.FontSize == 0 { - o.Title.SubtextStyle.FontSize = labelFontSize - } - if o.Title.SubtextStyle.Font == nil { - o.Title.SubtextStyle.Font = o.Font - } - - o.Legend.theme = theme - if o.Legend.Style.FontSize == 0 { - o.Legend.Style.FontSize = labelFontSize - } - if o.Legend.Left == "" { - o.Legend.Left = PositionCenter - } - // legend与series name的关联 - if len(o.Legend.Data) == 0 { - o.Legend.Data = o.SeriesList.Names() - } else { - seriesCount := len(o.SeriesList) - for index, name := range o.Legend.Data { - if index < seriesCount && - len(o.SeriesList[index].Name) == 0 { - o.SeriesList[index].Name = name - } - } - nameIndexDict := map[string]int{} - for index, name := range o.Legend.Data { - nameIndexDict[name] = index - } - // 保证series的顺序与legend一致 - sort.Slice(o.SeriesList, func(i, j int) bool { - return nameIndexDict[o.SeriesList[i].Name] < nameIndexDict[o.SeriesList[j].Name] - }) - } - // 如果无legend数据,则隐藏 - if len(strings.Join(o.Legend.Data, "")) == 0 { - o.Legend.Show = FalseFlag() - } - if o.Legend.Style.Font == nil { - o.Legend.Style.Font = o.Font - } - if o.Legend.Style.FontColor.IsZero() { - o.Legend.Style.FontColor = t.GetTextColor() - } - if o.XAxis.Theme == "" { - o.XAxis.Theme = theme - } - o.XAxis.Font = o.Font -} - -func (o *ChartOption) getWidth() int { - if o.Width != 0 { - return o.Width - } - if o.Parent != nil { - return o.Parent.Box.Width() - } - return defaultChartWidth -} - -func SetDefaultWidth(width int) { - if width > 0 { - defaultChartWidth = width - } -} -func SetDefaultHeight(height int) { - if height > 0 { - defaultChartHeight = height - } -} - -func (o *ChartOption) getHeight() int { - - if o.Height != 0 { - return o.Height - } - if o.Parent != nil { - return o.Parent.Box.Height() - } - return defaultChartHeight -} - -func (o *ChartOption) newYRange(axisIndex int) Range { - min := math.MaxFloat64 - max := -math.MaxFloat64 - if axisIndex >= len(o.YAxisList) { - axisIndex = 0 - } - yAxis := o.YAxisList[axisIndex] - - for _, series := range o.SeriesList { - if series.YAxisIndex != axisIndex { - continue - } - for _, item := range series.Data { - if item.Value > max { - max = item.Value - } - if item.Value < min { - min = item.Value - } - } - } - min = min * 0.9 - max = max * 1.1 - if yAxis.Min != nil { - min = *yAxis.Min - } - if yAxis.Max != nil { - max = *yAxis.Max - } - divideCount := 6 - // y轴分设置默认划分为6块 - r := NewRange(min, max, divideCount) - - // 由于NewRange会重新计算min max - if yAxis.Min != nil { - r.Min = min - } - if yAxis.Max != nil { - r.Max = max - } - - return r -} - -type basicRenderResult struct { - xRange *Range - yRangeList []*Range - d *Draw - titleBox chart.Box -} - -func (r *basicRenderResult) getYRange(index int) *Range { - if index >= len(r.yRangeList) { - index = 0 - } - return r.yRangeList[index] -} - -// Render renders the chart by option -func Render(opt ChartOption, optFuncs ...OptionFunc) (*Draw, error) { - for _, optFunc := range optFuncs { - optFunc(&opt) - } - if len(opt.SeriesList) == 0 { - return nil, errors.New("series can not be nil") - } - if len(opt.FontFamily) != 0 { - f, err := GetFont(opt.FontFamily) - if err != nil { - return nil, err - } - opt.Font = f - } - opt.FillDefault(opt.Theme) - - lineSeries := make([]Series, 0) - barSeries := make([]Series, 0) - isPieChart := false - isRadarChart := false - isFunnelChart := false - for index := range opt.SeriesList { - opt.SeriesList[index].index = index - item := opt.SeriesList[index] - switch item.Type { - case ChartTypePie: - isPieChart = true - case ChartTypeRadar: - isRadarChart = true - case ChartTypeFunnel: - isFunnelChart = true - case ChartTypeBar: - barSeries = append(barSeries, item) - default: - lineSeries = append(lineSeries, item) - } - } - // 如果指定了pie,则以pie的形式处理,pie不支持多类型图表 - // pie不需要axis - // radar 同样处理 - if isPieChart || - isRadarChart || - isFunnelChart { - opt.XAxis.Hidden = true - for index := range opt.YAxisList { - opt.YAxisList[index].Hidden = true - } - } - result, err := chartBasicRender(&opt) - if err != nil { - return nil, err - } - markPointRenderOptions := make([]markPointRenderOption, 0) - fns := []func() error{ - // pie render - func() error { - if !isPieChart { - return nil - } - return pieChartRender(pieChartOption{ - SeriesList: opt.SeriesList, - Theme: opt.Theme, - Font: opt.Font, - }, result) - }, - // radar render - func() error { - if !isRadarChart { - return nil - } - return radarChartRender(radarChartOption{ - SeriesList: opt.SeriesList, - Theme: opt.Theme, - Font: opt.Font, - Indicators: opt.RadarIndicators, - }, result) - }, - // funnel render - func() error { - if !isFunnelChart { - return nil - } - return funnelChartRender(funnelChartOption{ - SeriesList: opt.SeriesList, - Theme: opt.Theme, - Font: opt.Font, - }, result) - }, - // bar render - func() error { - // 如果无bar类型的series - if len(barSeries) == 0 { - return nil - } - options, err := barChartRender(barChartOption{ - SeriesList: barSeries, - Theme: opt.Theme, - Font: opt.Font, - }, result) - if err != nil { - return err - } - markPointRenderOptions = append(markPointRenderOptions, options...) - return nil - }, - // line render - func() error { - // 如果无line类型的series - if len(lineSeries) == 0 { - return nil - } - options, err := lineChartRender(lineChartOption{ - Theme: opt.Theme, - SeriesList: lineSeries, - Font: opt.Font, - }, result) - if err != nil { - return err - } - markPointRenderOptions = append(markPointRenderOptions, options...) - return nil - }, - // legend需要在顶层,因此此处render - func() error { - _, err := NewLegend(result.d, opt.Legend).Render() - return err - }, - // mark point最后render - func() error { - // mark point render不会出错 - for _, opt := range markPointRenderOptions { - markPointRender(opt) - } - return nil - }, - } - - for _, fn := range fns { - err = fn() - if err != nil { - return nil, err - } - } - for _, child := range opt.Children { - child.Parent = result.d - if len(child.Theme) == 0 { - child.Theme = opt.Theme - } - _, err = Render(child) - if err != nil { - return nil, err - } - } - return result.d, nil -} - -func chartBasicRender(opt *ChartOption) (*basicRenderResult, error) { - d, err := NewDraw( - DrawOption{ - Type: opt.Type, - Parent: opt.Parent, - Width: opt.getWidth(), - Height: opt.getHeight(), - }, - BoxOption(opt.Box), - PaddingOption(opt.Padding), - ) - if err != nil { - return nil, err - } - - if len(opt.YAxisList) > 2 { - return nil, errors.New("y axis should not be gt 2") - } - if opt.Parent == nil { - d.setBackground(opt.getWidth(), opt.getHeight(), opt.BackgroundColor) - } - - // 标题 - titleBox, err := drawTitle(d, &opt.Title) - if err != nil { - return nil, err - } - - xAxisHeight := 0 - var xRange *Range - - if !opt.XAxis.Hidden { - // xAxis - xAxisHeight, xRange, err = drawXAxis(d, &opt.XAxis, len(opt.YAxisList)) - if err != nil { - return nil, err - } - } - - yRangeList := make([]*Range, len(opt.YAxisList)) - - for index, yAxis := range opt.YAxisList { - var yRange *Range - if !yAxis.Hidden { - yRange, err = drawYAxis(d, opt, index, xAxisHeight, chart.Box{ - Top: titleBox.Height(), - }) - if err != nil { - return nil, err - } - yRangeList[index] = yRange - } - } - return &basicRenderResult{ - xRange: xRange, - yRangeList: yRangeList, - d: d, - titleBox: titleBox, - }, nil -} diff --git a/chart_option.go b/chart_option.go deleted file mode 100644 index 5e25873..0000000 --- a/chart_option.go +++ /dev/null @@ -1,190 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -// OptionFunc option function -type OptionFunc func(opt *ChartOption) - -// PNGTypeOption set png type of chart's output -func PNGTypeOption() OptionFunc { - return TypeOptionFunc(ChartOutputPNG) -} - -// TypeOptionFunc set type of chart's output -func TypeOptionFunc(t string) OptionFunc { - return func(opt *ChartOption) { - opt.Type = t - } -} - -// FontFamilyOptionFunc set font family of chart -func FontFamilyOptionFunc(fontFamily string) OptionFunc { - return func(opt *ChartOption) { - opt.FontFamily = fontFamily - } -} - -// ThemeOptionFunc set them of chart -func ThemeOptionFunc(theme string) OptionFunc { - return func(opt *ChartOption) { - opt.Theme = theme - } -} - -// TitleOptionFunc set title of chart -func TitleOptionFunc(title TitleOption) OptionFunc { - return func(opt *ChartOption) { - opt.Title = title - } -} - -// LegendOptionFunc set legend of chart -func LegendOptionFunc(legend LegendOption) OptionFunc { - return func(opt *ChartOption) { - opt.Legend = legend - } -} - -// XAxisOptionFunc set x axis of chart -func XAxisOptionFunc(xAxisOption XAxisOption) OptionFunc { - return func(opt *ChartOption) { - opt.XAxis = xAxisOption - } -} - -// YAxisOptionFunc set y axis of chart, support two y axis -func YAxisOptionFunc(yAxisOption ...YAxisOption) OptionFunc { - return func(opt *ChartOption) { - opt.YAxisList = yAxisOption - } -} - -// WidthOptionFunc set width of chart -func WidthOptionFunc(width int) OptionFunc { - return func(opt *ChartOption) { - opt.Width = width - } -} - -// HeightOptionFunc set height of chart -func HeightOptionFunc(height int) OptionFunc { - return func(opt *ChartOption) { - opt.Height = height - } -} - -// PaddingOptionFunc set padding of chart -func PaddingOptionFunc(padding chart.Box) OptionFunc { - return func(opt *ChartOption) { - opt.Padding = padding - } -} - -// BoxOptionFunc set box of chart -func BoxOptionFunc(box chart.Box) OptionFunc { - return func(opt *ChartOption) { - opt.Box = box - } -} - -// ChildOptionFunc add child chart -func ChildOptionFunc(child ...ChartOption) OptionFunc { - return func(opt *ChartOption) { - if opt.Children == nil { - opt.Children = make([]ChartOption, 0) - } - opt.Children = append(opt.Children, child...) - } -} - -// RadarIndicatorOptionFunc set radar indicator of chart -func RadarIndicatorOptionFunc(radarIndicator ...RadarIndicator) OptionFunc { - return func(opt *ChartOption) { - opt.RadarIndicators = radarIndicator - } -} - -// BackgroundColorOptionFunc set background color of chart -func BackgroundColorOptionFunc(color drawing.Color) OptionFunc { - return func(opt *ChartOption) { - opt.BackgroundColor = color - } -} - -// LineRender line chart render -func LineRender(values [][]float64, opts ...OptionFunc) (*Draw, error) { - seriesList := make(SeriesList, len(values)) - for index, value := range values { - seriesList[index] = NewSeriesFromValues(value, ChartTypeLine) - } - return Render(ChartOption{ - SeriesList: seriesList, - }, opts...) -} - -// BarRender bar chart render -func BarRender(values [][]float64, opts ...OptionFunc) (*Draw, error) { - seriesList := make(SeriesList, len(values)) - for index, value := range values { - seriesList[index] = NewSeriesFromValues(value, ChartTypeBar) - } - return Render(ChartOption{ - SeriesList: seriesList, - }, opts...) -} - -// PieRender pie chart render -func PieRender(values []float64, opts ...OptionFunc) (*Draw, error) { - return Render(ChartOption{ - SeriesList: NewPieSeriesList(values), - }, opts...) -} - -// RadarRender radar chart render -func RadarRender(values [][]float64, opts ...OptionFunc) (*Draw, error) { - seriesList := make(SeriesList, len(values)) - for index, value := range values { - seriesList[index] = NewSeriesFromValues(value, ChartTypeRadar) - } - return Render(ChartOption{ - SeriesList: seriesList, - }, opts...) -} - -// FunnelRender funnel chart render -func FunnelRender(values []float64, opts ...OptionFunc) (*Draw, error) { - seriesList := make(SeriesList, len(values)) - for index, value := range values { - seriesList[index] = NewSeriesFromValues([]float64{ - value, - }, ChartTypeFunnel) - } - return Render(ChartOption{ - SeriesList: seriesList, - }, opts...) -} diff --git a/chart_option_test.go b/chart_option_test.go deleted file mode 100644 index 41e8d50..0000000 --- a/chart_option_test.go +++ /dev/null @@ -1,238 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -func TestOptionFunc(t *testing.T) { - assert := assert.New(t) - - fns := []OptionFunc{ - TypeOptionFunc(ChartOutputPNG), - FontFamilyOptionFunc("fontFamily"), - ThemeOptionFunc("black"), - TitleOptionFunc(TitleOption{ - Text: "title", - }), - LegendOptionFunc(LegendOption{ - Data: []string{ - "a", - "b", - }, - }), - XAxisOptionFunc(NewXAxisOption([]string{ - "Mon", - "Tue", - })), - YAxisOptionFunc(YAxisOption{ - Min: NewFloatPoint(0), - Max: NewFloatPoint(100), - }), - WidthOptionFunc(400), - HeightOptionFunc(300), - PaddingOptionFunc(chart.Box{ - Top: 10, - }), - BoxOptionFunc(chart.Box{ - Left: 0, - Right: 300, - }), - ChildOptionFunc(ChartOption{}), - RadarIndicatorOptionFunc(RadarIndicator{ - Min: 0, - Max: 10, - }), - BackgroundColorOptionFunc(drawing.ColorBlack), - } - - opt := ChartOption{} - for _, fn := range fns { - fn(&opt) - } - - assert.Equal("png", opt.Type) - assert.Equal("fontFamily", opt.FontFamily) - assert.Equal("black", opt.Theme) - assert.Equal(TitleOption{ - Text: "title", - }, opt.Title) - assert.Equal(LegendOption{ - Data: []string{ - "a", - "b", - }, - }, opt.Legend) - assert.Equal(NewXAxisOption([]string{ - "Mon", - "Tue", - }), opt.XAxis) - assert.Equal([]YAxisOption{ - { - Min: NewFloatPoint(0), - Max: NewFloatPoint(100), - }, - }, opt.YAxisList) - assert.Equal(400, opt.Width) - assert.Equal(300, opt.Height) - assert.Equal(chart.Box{ - Top: 10, - }, opt.Padding) - assert.Equal(chart.Box{ - Left: 0, - Right: 300, - }, opt.Box) - assert.Equal(1, len(opt.Children)) - assert.Equal([]RadarIndicator{ - { - Min: 0, - Max: 10, - }, - }, opt.RadarIndicators) - assert.Equal(drawing.ColorBlack, opt.BackgroundColor) -} - -func TestLineRender(t *testing.T) { - assert := assert.New(t) - - d, err := LineRender([][]float64{ - { - 1, - 2, - 3, - }, - { - 1, - 5, - 2, - }, - }, - XAxisOptionFunc(NewXAxisOption([]string{ - "01", - "02", - "03", - })), - ) - assert.Nil(err) - data, err := d.Bytes() - assert.Nil(err) - assert.Equal("\\n010203024681012", string(data)) -} - -func TestBarRender(t *testing.T) { - assert := assert.New(t) - - d, err := BarRender([][]float64{ - { - 1, - 2, - 3, - }, - { - 1, - 5, - 2, - }, - }, - XAxisOptionFunc(NewXAxisOption([]string{ - "01", - "02", - "03", - })), - ) - assert.Nil(err) - data, err := d.Bytes() - assert.Nil(err) - assert.Equal("\\n010203024681012", string(data)) -} - -func TestPieRender(t *testing.T) { - assert := assert.New(t) - - d, err := PieRender([]float64{ - 1, - 3, - 5, - }) - assert.Nil(err) - data, err := d.Bytes() - assert.Nil(err) - assert.Equal("\\n", string(data)) -} - -func TestRadarRender(t *testing.T) { - assert := assert.New(t) - d, err := RadarRender([][]float64{ - { - 1, - 2, - 3, - }, - { - 1, - 5, - 2, - }, - }, - RadarIndicatorOptionFunc([]RadarIndicator{ - { - Name: "A", - Min: 0, - Max: 10, - }, - { - Name: "B", - Min: 0, - Max: 10, - }, - { - Name: "C", - Min: 0, - Max: 10, - }, - }...), - ) - assert.Nil(err) - data, err := d.Bytes() - assert.Nil(err) - assert.Equal("\\nABC", string(data)) -} - -func TestFunnelRender(t *testing.T) { - assert := assert.New(t) - - d, err := FunnelRender([]float64{ - 5, - 3, - 1, - }) - assert.Nil(err) - data, err := d.Bytes() - assert.Nil(err) - assert.Equal("\\n(100%)(60%)(20%)", string(data)) -} diff --git a/chart_test.go b/chart_test.go deleted file mode 100644 index c73745e..0000000 --- a/chart_test.go +++ /dev/null @@ -1,567 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "errors" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -func TestChartSetDefaultWidthHeight(t *testing.T) { - assert := assert.New(t) - - width := defaultChartWidth - height := defaultChartHeight - defer SetDefaultWidth(width) - defer SetDefaultHeight(height) - - SetDefaultWidth(60) - assert.Equal(60, defaultChartWidth) - SetDefaultHeight(40) - assert.Equal(40, defaultChartHeight) -} - -func TestChartFillDefault(t *testing.T) { - assert := assert.New(t) - // default value - opt := ChartOption{} - opt.FillDefault("") - // padding - assert.Equal(chart.Box{ - Top: 10, - Right: 10, - Bottom: 10, - Left: 10, - }, opt.Padding) - // background color - assert.Equal(drawing.ColorWhite, opt.BackgroundColor) - // title font color - assert.Equal(drawing.Color{ - R: 70, - G: 70, - B: 70, - A: 255, - }, opt.Title.Style.FontColor) - // title font size - assert.Equal(float64(14), opt.Title.Style.FontSize) - // sub title font color - assert.Equal(drawing.Color{ - R: 70, - G: 70, - B: 70, - A: 180, - }, opt.Title.SubtextStyle.FontColor) - // sub title font size - assert.Equal(float64(10), opt.Title.SubtextStyle.FontSize) - // legend font size - assert.Equal(float64(10), opt.Legend.Style.FontSize) - // legend position - assert.Equal("center", opt.Legend.Left) - assert.Equal(drawing.Color{ - R: 70, - G: 70, - B: 70, - A: 255, - }, opt.Legend.Style.FontColor) - - // y axis - opt = ChartOption{ - SeriesList: SeriesList{ - { - YAxisIndex: 1, - }, - }, - } - opt.FillDefault("") - assert.Equal([]YAxisOption{ - {}, - {}, - }, opt.YAxisList) - opt = ChartOption{} - opt.FillDefault("") - assert.Equal([]YAxisOption{ - {}, - }, opt.YAxisList) - - // legend get from series's name - - opt = ChartOption{ - SeriesList: SeriesList{ - { - Name: "a", - }, - { - Name: "b", - }, - }, - } - opt.FillDefault("") - assert.Equal([]string{ - "a", - "b", - }, opt.Legend.Data) - // series name set by legend - opt = ChartOption{ - Legend: LegendOption{ - Data: []string{ - "a", - "b", - }, - }, - SeriesList: SeriesList{ - {}, - {}, - }, - } - opt.FillDefault("") - assert.Equal("a", opt.SeriesList[0].Name) - assert.Equal("b", opt.SeriesList[1].Name) -} - -func TestChartGetWidthHeight(t *testing.T) { - assert := assert.New(t) - - opt := ChartOption{ - Width: 10, - } - assert.Equal(10, opt.getWidth()) - opt.Width = 0 - assert.Equal(600, opt.getWidth()) - opt.Parent = &Draw{ - Box: chart.Box{ - Left: 10, - Right: 50, - }, - } - assert.Equal(40, opt.getWidth()) - - opt = ChartOption{ - Height: 20, - } - assert.Equal(20, opt.getHeight()) - opt.Height = 0 - assert.Equal(400, opt.getHeight()) - opt.Parent = &Draw{ - Box: chart.Box{ - Top: 20, - Bottom: 80, - }, - } - assert.Equal(60, opt.getHeight()) -} - -func TestChartRender(t *testing.T) { - assert := assert.New(t) - - d, err := Render(ChartOption{ - Width: 800, - Height: 600, - Legend: LegendOption{ - Top: "-90", - Data: []string{ - "Milk Tea", - "Matcha Latte", - "Cheese Cocoa", - "Walnut Brownie", - }, - }, - Padding: chart.Box{ - Top: 100, - }, - XAxis: NewXAxisOption([]string{ - "2012", - "2013", - "2014", - "2015", - "2016", - "2017", - }), - YAxisList: []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%", - }), - }, - { - Legend: NewLegendOption([]string{ - "Allocated Budget", - "Actual Spending", - }), - Box: chart.Box{ - Top: 20, - Left: 0, - Right: 200, - Bottom: 120, - }, - RadarIndicators: []RadarIndicator{ - { - Name: "Sales", - Max: 6500, - }, - { - Name: "Administration", - Max: 16000, - }, - { - Name: "Information Technology", - Max: 30000, - }, - { - Name: "Customer Support", - Max: 38000, - }, - { - Name: "Development", - Max: 52000, - }, - { - Name: "Marketing", - Max: 25000, - }, - }, - SeriesList: SeriesList{ - { - Type: ChartTypeRadar, - Data: NewSeriesDataFromValues([]float64{ - 4200, - 3000, - 20000, - 35000, - 50000, - 18000, - }), - }, - { - Type: ChartTypeRadar, - index: 1, - Data: NewSeriesDataFromValues([]float64{ - 5000, - 14000, - 28000, - 26000, - 42000, - 21000, - }), - }, - }, - }, - }, - }) - assert.Nil(err) - data, err := d.Bytes() - assert.Nil(err) - assert.Equal("\\n2012201320142015201620170153045607590Milk TeaMatcha LatteCheese CocoaWalnut BrownieMilk Tea: 34.03%Matcha Latte: 27.66%Cheese Cocoa: 22.32%Walnut Brownie: 15.96%SalesAdministrationInformation TechnologyCustomer SupportDevelopmentMarketingAllocated BudgetActual Spending", string(data)) -} - -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", - }), - YAxisList: []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", - }), - YAxisList: []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/draw.go b/draw.go deleted file mode 100644 index 1708662..0000000 --- a/draw.go +++ /dev/null @@ -1,372 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "bytes" - "errors" - "math" - - "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -const ( - PositionLeft = "left" - PositionRight = "right" - PositionCenter = "center" - PositionTop = "top" - PositionBottom = "bottom" -) - -const ( - OrientHorizontal = "horizontal" - OrientVertical = "vertical" -) - -type Draw struct { - // Render - Render chart.Renderer - // The canvas box - Box chart.Box - // The font for draw - Font *truetype.Font - // The parent of draw - parent *Draw -} - -type DrawOption struct { - // Draw type, "svg" or "png", default type is "svg" - Type string - // Parent of draw - Parent *Draw - // The width of draw canvas - Width int - // The height of draw canvas - Height int -} - -type Option func(*Draw) error - -// PaddingOption sets the padding of draw canvas -func PaddingOption(padding chart.Box) Option { - return func(d *Draw) error { - d.Box.Left += padding.Left - d.Box.Top += padding.Top - d.Box.Right -= padding.Right - d.Box.Bottom -= padding.Bottom - return nil - } -} - -// BoxOption set the box of draw canvas -func BoxOption(box chart.Box) Option { - return func(d *Draw) error { - if box.IsZero() { - return nil - } - d.Box = box - return nil - } -} - -// NewDraw returns a new draw canvas -func NewDraw(opt DrawOption, opts ...Option) (*Draw, error) { - if opt.Parent == nil && (opt.Width <= 0 || opt.Height <= 0) { - return nil, errors.New("parent and width/height can not be nil") - } - font, _ := chart.GetDefaultFont() - d := &Draw{ - Font: font, - } - width := opt.Width - height := opt.Height - if opt.Parent != nil { - d.parent = opt.Parent - d.Render = d.parent.Render - d.Box = opt.Parent.Box.Clone() - } - if width != 0 && height != 0 { - d.Box.Right = width + d.Box.Left - d.Box.Bottom = height + d.Box.Top - } - // 创建render - if d.parent == nil { - fn := chart.SVG - if opt.Type == ChartOutputPNG { - fn = chart.PNG - } - r, err := fn(d.Box.Right, d.Box.Bottom) - if err != nil { - return nil, err - } - d.Render = r - } - - for _, o := range opts { - err := o(d) - if err != nil { - return nil, err - } - } - return d, nil -} - -// Parent returns the parent of draw -func (d *Draw) Parent() *Draw { - return d.parent -} - -// Top returns the top parent of draw -func (d *Draw) Top() *Draw { - if d.parent == nil { - return nil - } - t := d.parent - // 限制最多查询次数,避免嵌套引用 - for i := 50; i > 0; i-- { - if t.parent == nil { - break - } - t = t.parent - } - return t -} - -// Bytes returns the data of draw canvas -func (d *Draw) Bytes() ([]byte, error) { - buffer := bytes.Buffer{} - err := d.Render.Save(&buffer) - if err != nil { - return nil, err - } - return buffer.Bytes(), err -} - -func (d *Draw) moveTo(x, y int) { - d.Render.MoveTo(x+d.Box.Left, y+d.Box.Top) -} - -func (d *Draw) arcTo(cx, cy int, rx, ry, startAngle, delta float64) { - d.Render.ArcTo(cx+d.Box.Left, cy+d.Box.Top, rx, ry, startAngle, delta) -} - -func (d *Draw) lineTo(x, y int) { - d.Render.LineTo(x+d.Box.Left, y+d.Box.Top) -} - -func (d *Draw) pin(x, y, width int) { - r := float64(width) / 2 - y -= width / 4 - angle := chart.DegreesToRadians(15) - - startAngle := math.Pi/2 + angle - delta := 2*math.Pi - 2*angle - d.arcTo(x, y, r, r, startAngle, delta) - d.lineTo(x, y) - d.Render.Close() - d.Render.FillStroke() - - startX := x - int(r) - startY := y - endX := x + int(r) - endY := y - d.moveTo(startX, startY) - - left := d.Box.Left - top := d.Box.Top - cx := x - cy := y + int(r*2.5) - d.Render.QuadCurveTo(cx+left, cy+top, endX+left, endY+top) - d.Render.Close() - d.Render.Fill() -} - -func (d *Draw) arrowLeft(x, y, width, height int) { - d.arrow(x, y, width, height, PositionLeft) -} - -func (d *Draw) arrowRight(x, y, width, height int) { - d.arrow(x, y, width, height, PositionRight) -} - -func (d *Draw) arrowTop(x, y, width, height int) { - d.arrow(x, y, width, height, PositionTop) -} -func (d *Draw) arrowBottom(x, y, width, height int) { - d.arrow(x, y, width, height, PositionBottom) -} - -func (d *Draw) arrow(x, y, width, height int, direction string) { - halfWidth := width >> 1 - halfHeight := height >> 1 - if direction == PositionTop || direction == PositionBottom { - x0 := x - halfWidth - x1 := x0 + width - dy := -height / 3 - y0 := y - y1 := y0 - height - if direction == PositionBottom { - y0 = y - height - y1 = y - dy = 2 * dy - } - d.moveTo(x0, y0) - d.lineTo(x0+halfWidth, y1) - d.lineTo(x1, y0) - d.lineTo(x0+halfWidth, y+dy) - d.lineTo(x0, y0) - } else { - x0 := x + width - x1 := x0 - width - y0 := y - halfHeight - dx := -width / 3 - if direction == PositionRight { - x0 = x - width - dx = -dx - x1 = x0 + width - } - d.moveTo(x0, y0) - d.lineTo(x1, y0+halfHeight) - d.lineTo(x0, y0+height) - d.lineTo(x0+dx, y0+halfHeight) - d.lineTo(x0, y0) - } - d.Render.FillStroke() -} - -func (d *Draw) makeLine(x, y, width int) { - arrowWidth := 16 - arrowHeight := 10 - endX := x + width - d.circle(3, x, y) - d.Render.Fill() - d.moveTo(x+5, y) - d.lineTo(endX-arrowWidth, y) - d.Render.Stroke() - d.Render.SetStrokeDashArray([]float64{}) - d.arrowRight(endX, y, arrowWidth, arrowHeight) -} - -func (d *Draw) circle(radius float64, x, y int) { - d.Render.Circle(radius, x+d.Box.Left, y+d.Box.Top) -} - -func (d *Draw) text(body string, x, y int) { - d.Render.Text(body, x+d.Box.Left, y+d.Box.Top) -} - -func (d *Draw) textFit(body string, x, y, width int, style chart.Style) chart.Box { - style.TextWrap = chart.TextWrapWord - r := d.Render - lines := chart.Text.WrapFit(r, body, width, style) - style.WriteTextOptionsToRenderer(r) - var output chart.Box - - for index, line := range lines { - x0 := x - y0 := y + output.Height() - d.text(line, x0, y0) - lineBox := r.MeasureText(line) - output.Right = chart.MaxInt(lineBox.Right, output.Right) - output.Bottom += lineBox.Height() - if index < len(lines)-1 { - output.Bottom += +style.GetTextLineSpacing() - } - } - return output -} - -func (d *Draw) measureTextFit(body string, x, y, width int, style chart.Style) chart.Box { - style.TextWrap = chart.TextWrapWord - r := d.Render - lines := chart.Text.WrapFit(r, body, width, style) - style.WriteTextOptionsToRenderer(r) - return chart.Text.MeasureLines(r, lines, style) -} - -func (d *Draw) lineStroke(points []Point, style LineStyle) { - s := style.Style() - if !s.ShouldDrawStroke() { - return - } - r := d.Render - s.GetStrokeOptions().WriteDrawingOptionsToRenderer(r) - for index, point := range points { - x := point.X - y := point.Y - if index == 0 { - d.moveTo(x, y) - } else { - d.lineTo(x, y) - } - } - r.Stroke() -} - -func (d *Draw) setBackground(width, height int, color drawing.Color) { - r := d.Render - s := chart.Style{ - FillColor: color, - } - s.WriteToRenderer(r) - r.MoveTo(0, 0) - r.LineTo(width, 0) - r.LineTo(width, height) - r.LineTo(0, height) - r.LineTo(0, 0) - r.FillStroke() -} - -func (d *Draw) polygon(center Point, radius float64, sides int) { - points := getPolygonPoints(center, radius, sides) - for i, p := range points { - if i == 0 { - d.moveTo(p.X, p.Y) - } else { - d.lineTo(p.X, p.Y) - } - } - d.lineTo(points[0].X, points[0].Y) - d.Render.Stroke() -} - -func (d *Draw) fill(points []Point, s chart.Style) { - if !s.ShouldDrawFill() { - return - } - r := d.Render - var x, y int - s.GetFillOptions().WriteDrawingOptionsToRenderer(r) - for index, point := range points { - x = point.X - y = point.Y - if index == 0 { - d.moveTo(x, y) - } else { - d.lineTo(x, y) - } - } - r.Fill() -} diff --git a/draw_test.go b/draw_test.go deleted file mode 100644 index f6a3dd1..0000000 --- a/draw_test.go +++ /dev/null @@ -1,507 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "math" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -func TestParentOption(t *testing.T) { - assert := assert.New(t) - p, err := NewDraw(DrawOption{ - Width: 400, - Height: 300, - }) - assert.Nil(err) - - d, err := NewDraw(DrawOption{ - Parent: p, - }) - assert.Nil(err) - assert.Equal(p, d.parent) -} - -func TestWidthHeightOption(t *testing.T) { - assert := assert.New(t) - - // no parent - width := 300 - height := 200 - d, err := NewDraw(DrawOption{ - Width: width, - Height: height, - }) - assert.Nil(err) - assert.Equal(chart.Box{ - Top: 0, - Left: 0, - Right: width, - Bottom: height, - }, d.Box) - - width = 500 - height = 600 - // with parent - p, err := NewDraw( - DrawOption{ - Width: width, - Height: height, - }, - PaddingOption(chart.NewBox(5, 5, 5, 5)), - ) - assert.Nil(err) - d, err = NewDraw( - DrawOption{ - Parent: p, - }, - PaddingOption(chart.NewBox(1, 2, 3, 4)), - ) - assert.Nil(err) - assert.Equal(chart.Box{ - Top: 6, - Left: 7, - Right: 492, - Bottom: 591, - }, d.Box) -} - -func TestBoxOption(t *testing.T) { - assert := assert.New(t) - - d, err := NewDraw(DrawOption{ - Width: 400, - Height: 300, - }) - assert.Nil(err) - - err = BoxOption(chart.Box{ - Left: 10, - Top: 20, - Right: 50, - Bottom: 100, - })(d) - assert.Nil(err) - assert.Equal(chart.Box{ - Left: 10, - Top: 20, - Right: 50, - Bottom: 100, - }, d.Box) - - // zero box will be ignored - err = BoxOption(chart.Box{})(d) - assert.Nil(err) - assert.Equal(chart.Box{ - Left: 10, - Top: 20, - Right: 50, - Bottom: 100, - }, d.Box) -} - -func TestPaddingOption(t *testing.T) { - assert := assert.New(t) - - d, err := NewDraw(DrawOption{ - Width: 400, - Height: 300, - }) - assert.Nil(err) - - // 默认的box - assert.Equal(chart.Box{ - Right: 400, - Bottom: 300, - }, d.Box) - - // 设置padding之后的box - d, err = NewDraw(DrawOption{ - Width: 400, - Height: 300, - }, PaddingOption(chart.Box{ - Left: 1, - Top: 2, - Right: 3, - Bottom: 4, - })) - assert.Nil(err) - assert.Equal(chart.Box{ - Top: 2, - Left: 1, - Right: 397, - Bottom: 296, - }, d.Box) - - p := d - // 设置父元素之后的box - d, err = NewDraw( - DrawOption{ - Parent: p, - }, - PaddingOption(chart.Box{ - Left: 1, - Top: 2, - Right: 3, - Bottom: 4, - }), - ) - assert.Nil(err) - assert.Equal(chart.Box{ - Top: 4, - Left: 2, - Right: 394, - Bottom: 292, - }, d.Box) -} - -func TestParentTop(t *testing.T) { - assert := assert.New(t) - d1, err := NewDraw(DrawOption{ - Width: 400, - Height: 300, - }) - assert.Nil(err) - - d2, err := NewDraw(DrawOption{ - Parent: d1, - }) - assert.Nil(err) - - d3, err := NewDraw(DrawOption{ - Parent: d2, - }) - assert.Nil(err) - - assert.Equal(d2, d3.Parent()) - assert.Equal(d1, d2.Parent()) - assert.Equal(d1, d3.Top()) - assert.Equal(d1, d2.Top()) -} - -func TestDraw(t *testing.T) { - assert := assert.New(t) - - tests := []struct { - fn func(d *Draw) - result string - }{ - // moveTo, lineTo - { - fn: func(d *Draw) { - d.moveTo(1, 1) - d.lineTo(2, 2) - d.Render.Stroke() - }, - result: "\\n", - }, - // circle - { - fn: func(d *Draw) { - d.circle(5, 2, 3) - }, - result: "\\n", - }, - // text - { - fn: func(d *Draw) { - d.text("hello world!", 3, 6) - }, - result: "\\nhello world!", - }, - // line stroke - { - fn: func(d *Draw) { - d.lineStroke([]Point{ - { - X: 1, - Y: 2, - }, - { - X: 3, - Y: 4, - }, - }, LineStyle{ - StrokeColor: drawing.ColorBlack, - StrokeWidth: 1, - }) - }, - result: "\\n", - }, - // set background - { - fn: func(d *Draw) { - d.setBackground(400, 300, chart.ColorWhite) - }, - result: "\\n", - }, - // arcTo - { - fn: func(d *Draw) { - chart.Style{ - StrokeWidth: 1, - StrokeColor: drawing.ColorBlack, - FillColor: drawing.ColorBlue, - }.WriteToRenderer(d.Render) - d.arcTo(100, 100, 100, 100, 0, math.Pi/2) - d.Render.Close() - d.Render.FillStroke() - }, - result: "\\n", - }, - // pin - { - fn: func(d *Draw) { - chart.Style{ - StrokeWidth: 1, - StrokeColor: drawing.Color{ - R: 84, - G: 112, - B: 198, - A: 255, - }, - FillColor: drawing.Color{ - R: 84, - G: 112, - B: 198, - A: 255, - }, - }.WriteToRenderer(d.Render) - d.pin(30, 30, 30) - }, - result: "\\n", - }, - // arrow left - { - fn: func(d *Draw) { - chart.Style{ - StrokeWidth: 1, - StrokeColor: drawing.Color{ - R: 84, - G: 112, - B: 198, - A: 255, - }, - FillColor: drawing.Color{ - R: 84, - G: 112, - B: 198, - A: 255, - }, - }.WriteToRenderer(d.Render) - d.arrowLeft(30, 30, 16, 10) - }, - result: "\\n", - }, - // arrow right - { - fn: func(d *Draw) { - chart.Style{ - StrokeWidth: 1, - StrokeColor: drawing.Color{ - R: 84, - G: 112, - B: 198, - A: 255, - }, - FillColor: drawing.Color{ - R: 84, - G: 112, - B: 198, - A: 255, - }, - }.WriteToRenderer(d.Render) - d.arrowRight(30, 30, 16, 10) - }, - result: "\\n", - }, - // arrow top - { - fn: func(d *Draw) { - chart.Style{ - StrokeWidth: 1, - StrokeColor: drawing.Color{ - R: 84, - G: 112, - B: 198, - A: 255, - }, - FillColor: drawing.Color{ - R: 84, - G: 112, - B: 198, - A: 255, - }, - }.WriteToRenderer(d.Render) - d.arrowTop(30, 30, 10, 16) - }, - result: "\\n", - }, - // arrow bottom - { - fn: func(d *Draw) { - chart.Style{ - StrokeWidth: 1, - StrokeColor: drawing.Color{ - R: 84, - G: 112, - B: 198, - A: 255, - }, - FillColor: drawing.Color{ - R: 84, - G: 112, - B: 198, - A: 255, - }, - }.WriteToRenderer(d.Render) - d.arrowBottom(30, 30, 10, 16) - }, - result: "\\n", - }, - // mark line - { - fn: func(d *Draw) { - chart.Style{ - StrokeWidth: 1, - StrokeColor: drawing.Color{ - R: 84, - G: 112, - B: 198, - A: 255, - }, - FillColor: drawing.Color{ - R: 84, - G: 112, - B: 198, - A: 255, - }, - StrokeDashArray: []float64{ - 4, - 2, - }, - }.WriteToRenderer(d.Render) - d.makeLine(0, 20, 300) - }, - result: "\\n", - }, - // polygon - { - fn: func(d *Draw) { - chart.Style{ - StrokeWidth: 1, - StrokeColor: drawing.Color{ - R: 84, - G: 112, - B: 198, - A: 255, - }, - }.WriteToRenderer(d.Render) - d.polygon(Point{ - X: 100, - Y: 100, - }, 50, 6) - }, - result: "\\n", - }, - // fill - { - fn: func(d *Draw) { - d.fill([]Point{ - { - X: 0, - Y: 0, - }, - { - X: 0, - Y: 100, - }, - { - X: 100, - Y: 100, - }, - { - X: 0, - Y: 0, - }, - }, chart.Style{ - FillColor: drawing.Color{ - R: 84, - G: 112, - B: 198, - A: 255, - }, - }) - }, - result: "\\n", - }, - } - for _, tt := range tests { - d, err := NewDraw(DrawOption{ - Width: 400, - Height: 300, - }, PaddingOption(chart.Box{ - Left: 5, - Top: 10, - })) - assert.Nil(err) - tt.fn(d) - data, err := d.Bytes() - assert.Nil(err) - assert.Equal(tt.result, string(data)) - } -} - -func TestDrawTextFit(t *testing.T) { - assert := assert.New(t) - d, err := NewDraw(DrawOption{ - Width: 400, - Height: 300, - }) - assert.Nil(err) - f, _ := chart.GetDefaultFont() - style := chart.Style{ - FontSize: 12, - FontColor: chart.ColorBlack, - Font: f, - } - box := d.textFit("Hello World!", 0, 20, 80, style) - assert.Equal(chart.Box{ - Right: 45, - Bottom: 35, - }, box) - - box = d.textFit("Hello World!", 0, 100, 200, style) - assert.Equal(chart.Box{ - Right: 84, - Bottom: 15, - }, box) - - buf, err := d.Bytes() - assert.Nil(err) - assert.Equal(`\nHelloWorld!Hello World!`, string(buf)) -} diff --git a/echarts.go b/echarts.go deleted file mode 100644 index 4ebb9ad..0000000 --- a/echarts.go +++ /dev/null @@ -1,499 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "bytes" - "encoding/json" - "fmt" - "regexp" - "strconv" - - "github.com/wcharczuk/go-chart/v2" -) - -func convertToArray(data []byte) []byte { - data = bytes.TrimSpace(data) - if len(data) == 0 { - return nil - } - if data[0] != '[' { - data = []byte("[" + string(data) + "]") - } - return data -} - -type EChartsPosition string - -func (p *EChartsPosition) UnmarshalJSON(data []byte) error { - if len(data) == 0 { - return nil - } - if regexp.MustCompile(`^\d+`).Match(data) { - data = []byte(fmt.Sprintf(`"%s"`, string(data))) - } - s := (*string)(p) - return json.Unmarshal(data, s) -} - -type EChartStyle struct { - Color string `json:"color"` -} - -func (es *EChartStyle) ToStyle() chart.Style { - color := parseColor(es.Color) - return chart.Style{ - FillColor: color, - FontColor: color, - StrokeColor: color, - } -} - -type EChartsSeriesDataValue struct { - values []float64 -} - -func (value *EChartsSeriesDataValue) UnmarshalJSON(data []byte) error { - data = convertToArray(data) - return json.Unmarshal(data, &value.values) -} -func (value *EChartsSeriesDataValue) First() float64 { - if len(value.values) == 0 { - return 0 - } - return value.values[0] -} -func NewEChartsSeriesDataValue(values ...float64) EChartsSeriesDataValue { - return EChartsSeriesDataValue{ - values: values, - } -} - -type EChartsSeriesData struct { - Value EChartsSeriesDataValue `json:"value"` - Name string `json:"name"` - ItemStyle EChartStyle `json:"itemStyle"` -} -type _EChartsSeriesData EChartsSeriesData - -var numericRep = regexp.MustCompile(`^[-+]?[0-9]+(?:\.[0-9]+)?$`) - -func (es *EChartsSeriesData) UnmarshalJSON(data []byte) error { - data = bytes.TrimSpace(data) - if len(data) == 0 { - return nil - } - if numericRep.Match(data) { - v, err := strconv.ParseFloat(string(data), 64) - if err != nil { - return err - } - es.Value = EChartsSeriesDataValue{ - values: []float64{ - v, - }, - } - return nil - } - v := _EChartsSeriesData{} - err := json.Unmarshal(data, &v) - if err != nil { - return err - } - es.Name = v.Name - es.Value = v.Value - es.ItemStyle = v.ItemStyle - return nil -} - -type EChartsXAxisData struct { - BoundaryGap *bool `json:"boundaryGap"` - SplitNumber int `json:"splitNumber"` - Data []string `json:"data"` -} -type EChartsXAxis struct { - Data []EChartsXAxisData -} - -func (ex *EChartsXAxis) UnmarshalJSON(data []byte) error { - data = convertToArray(data) - if len(data) == 0 { - return nil - } - return json.Unmarshal(data, &ex.Data) -} - -type EChartsAxisLabel struct { - Formatter string `json:"formatter"` -} -type EChartsYAxisData struct { - Min *float64 `json:"min"` - Max *float64 `json:"max"` - AxisLabel EChartsAxisLabel `json:"axisLabel"` - AxisLine struct { - LineStyle struct { - Color string `json:"color"` - } `json:"lineStyle"` - } `json:"axisLine"` -} -type EChartsYAxis struct { - Data []EChartsYAxisData `json:"data"` -} - -func (ey *EChartsYAxis) UnmarshalJSON(data []byte) error { - data = convertToArray(data) - if len(data) == 0 { - return nil - } - return json.Unmarshal(data, &ey.Data) -} - -type EChartsPadding struct { - Box chart.Box -} - -func (eb *EChartsPadding) UnmarshalJSON(data []byte) error { - data = convertToArray(data) - if len(data) == 0 { - return nil - } - arr := make([]int, 0) - err := json.Unmarshal(data, &arr) - if err != nil { - return err - } - if len(arr) == 0 { - return nil - } - switch len(arr) { - case 1: - eb.Box = chart.Box{ - Left: arr[0], - Top: arr[0], - Bottom: arr[0], - Right: arr[0], - } - case 2: - eb.Box = chart.Box{ - Top: arr[0], - Bottom: arr[0], - Left: arr[1], - Right: arr[1], - } - default: - result := make([]int, 4) - copy(result, arr) - if len(arr) == 3 { - result[3] = result[1] - } - // 上右下左 - eb.Box = chart.Box{ - Top: result[0], - Right: result[1], - Bottom: result[2], - Left: result[3], - } - } - return nil -} - -type EChartsLabelOption struct { - Show bool `json:"show"` - Distance int `json:"distance"` - Color string `json:"color"` -} -type EChartsLegend struct { - Show *bool `json:"show"` - Data []string `json:"data"` - Align string `json:"align"` - Orient string `json:"orient"` - Padding EChartsPadding `json:"padding"` - Left EChartsPosition `json:"left"` - Top EChartsPosition `json:"top"` - TextStyle EChartsTextStyle `json:"textStyle"` -} - -type EChartsMarkData struct { - Type string `json:"type"` -} -type _EChartsMarkData EChartsMarkData - -func (emd *EChartsMarkData) UnmarshalJSON(data []byte) error { - data = bytes.TrimSpace(data) - if len(data) == 0 { - return nil - } - data = convertToArray(data) - ds := make([]*_EChartsMarkData, 0) - err := json.Unmarshal(data, &ds) - if err != nil { - return err - } - for _, d := range ds { - if d.Type != "" { - emd.Type = d.Type - } - } - return nil -} - -type EChartsMarkPoint struct { - SymbolSize int `json:"symbolSize"` - Data []EChartsMarkData `json:"data"` -} - -func (emp *EChartsMarkPoint) ToSeriesMarkPoint() SeriesMarkPoint { - sp := SeriesMarkPoint{ - SymbolSize: emp.SymbolSize, - } - if len(emp.Data) == 0 { - return sp - } - data := make([]SeriesMarkData, len(emp.Data)) - for index, item := range emp.Data { - data[index].Type = item.Type - } - sp.Data = data - return sp -} - -type EChartsMarkLine struct { - Data []EChartsMarkData `json:"data"` -} - -func (eml *EChartsMarkLine) ToSeriesMarkLine() SeriesMarkLine { - sl := SeriesMarkLine{} - if len(eml.Data) == 0 { - return sl - } - data := make([]SeriesMarkData, len(eml.Data)) - for index, item := range eml.Data { - data[index].Type = item.Type - } - sl.Data = data - return sl -} - -type EChartsSeries struct { - Data []EChartsSeriesData `json:"data"` - Name string `json:"name"` - Type string `json:"type"` - Radius string `json:"radius"` - YAxisIndex int `json:"yAxisIndex"` - ItemStyle EChartStyle `json:"itemStyle"` - // label的配置 - Label EChartsLabelOption `json:"label"` - MarkPoint EChartsMarkPoint `json:"markPoint"` - MarkLine EChartsMarkLine `json:"markLine"` - Max *float64 `json:"max"` - Min *float64 `json:"min"` -} -type EChartsSeriesList []EChartsSeries - -func (esList EChartsSeriesList) ToSeriesList() SeriesList { - seriesList := make(SeriesList, 0, len(esList)) - for _, item := range esList { - // 如果是pie,则每个子荐生成一个series - if item.Type == ChartTypePie { - for _, dataItem := range item.Data { - seriesList = append(seriesList, Series{ - Type: item.Type, - Name: dataItem.Name, - Label: SeriesLabel{ - Show: true, - }, - Radius: item.Radius, - Data: []SeriesData{ - { - Value: dataItem.Value.First(), - }, - }, - }) - } - continue - } - // 如果是radar或funnel - if item.Type == ChartTypeRadar || - item.Type == ChartTypeFunnel { - for _, dataItem := range item.Data { - seriesList = append(seriesList, Series{ - Name: dataItem.Name, - Type: item.Type, - Data: NewSeriesDataFromValues(dataItem.Value.values), - Max: item.Max, - Min: item.Min, - }) - } - continue - } - data := make([]SeriesData, len(item.Data)) - for j, dataItem := range item.Data { - data[j] = SeriesData{ - Value: dataItem.Value.First(), - Style: dataItem.ItemStyle.ToStyle(), - } - } - seriesList = append(seriesList, Series{ - Type: item.Type, - Data: data, - YAxisIndex: item.YAxisIndex, - Style: item.ItemStyle.ToStyle(), - Label: SeriesLabel{ - Color: parseColor(item.Label.Color), - Show: item.Label.Show, - Distance: item.Label.Distance, - }, - Name: item.Name, - MarkPoint: item.MarkPoint.ToSeriesMarkPoint(), - MarkLine: item.MarkLine.ToSeriesMarkLine(), - }) - } - return seriesList -} - -type EChartsTextStyle struct { - Color string `json:"color"` - FontFamily string `json:"fontFamily"` - FontSize float64 `json:"fontSize"` -} - -func (et *EChartsTextStyle) ToStyle() chart.Style { - s := chart.Style{ - FontSize: et.FontSize, - FontColor: parseColor(et.Color), - } - if et.FontFamily != "" { - s.Font, _ = GetFont(et.FontFamily) - } - return s -} - -type EChartsOption struct { - Type string `json:"type"` - Theme string `json:"theme"` - FontFamily string `json:"fontFamily"` - Padding EChartsPadding `json:"padding"` - Box chart.Box `json:"box"` - Width int `json:"width"` - Height int `json:"height"` - Title struct { - Text string `json:"text"` - Subtext string `json:"subtext"` - Left EChartsPosition `json:"left"` - Top EChartsPosition `json:"top"` - TextStyle EChartsTextStyle `json:"textStyle"` - SubtextStyle EChartsTextStyle `json:"subtextStyle"` - } `json:"title"` - XAxis EChartsXAxis `json:"xAxis"` - YAxis EChartsYAxis `json:"yAxis"` - Legend EChartsLegend `json:"legend"` - Radar struct { - Indicator []RadarIndicator `json:"indicator"` - } `json:"radar"` - Series EChartsSeriesList `json:"series"` - Children []EChartsOption `json:"children"` -} - -func (eo *EChartsOption) ToOption() ChartOption { - fontFamily := eo.FontFamily - if len(fontFamily) == 0 { - fontFamily = eo.Title.TextStyle.FontFamily - } - o := ChartOption{ - Type: eo.Type, - FontFamily: fontFamily, - Theme: eo.Theme, - Title: TitleOption{ - Text: eo.Title.Text, - Subtext: eo.Title.Subtext, - Style: eo.Title.TextStyle.ToStyle(), - SubtextStyle: eo.Title.SubtextStyle.ToStyle(), - Left: string(eo.Title.Left), - Top: string(eo.Title.Top), - }, - Legend: LegendOption{ - Show: eo.Legend.Show, - Style: eo.Legend.TextStyle.ToStyle(), - Data: eo.Legend.Data, - Left: string(eo.Legend.Left), - Top: string(eo.Legend.Top), - Align: eo.Legend.Align, - Orient: eo.Legend.Orient, - }, - RadarIndicators: eo.Radar.Indicator, - Width: eo.Width, - Height: eo.Height, - Padding: eo.Padding.Box, - Box: eo.Box, - SeriesList: eo.Series.ToSeriesList(), - } - if len(eo.XAxis.Data) != 0 { - xAxisData := eo.XAxis.Data[0] - o.XAxis = XAxisOption{ - BoundaryGap: xAxisData.BoundaryGap, - Data: xAxisData.Data, - SplitNumber: xAxisData.SplitNumber, - } - } - yAxisOptions := make([]YAxisOption, len(eo.YAxis.Data)) - for index, item := range eo.YAxis.Data { - yAxisOptions[index] = YAxisOption{ - Min: item.Min, - Max: item.Max, - Formatter: item.AxisLabel.Formatter, - Color: parseColor(item.AxisLine.LineStyle.Color), - } - } - o.YAxisList = yAxisOptions - - if len(eo.Children) != 0 { - o.Children = make([]ChartOption, len(eo.Children)) - for index, item := range eo.Children { - o.Children[index] = item.ToOption() - } - } - return o -} - -func renderEcharts(options, outputType string) ([]byte, error) { - o := EChartsOption{} - err := json.Unmarshal([]byte(options), &o) - if err != nil { - return nil, err - } - opt := o.ToOption() - opt.Type = outputType - d, err := Render(opt) - if err != nil { - return nil, err - } - return d.Bytes() -} - -func RenderEChartsToPNG(options string) ([]byte, error) { - return renderEcharts(options, "png") -} - -func RenderEChartsToSVG(options string) ([]byte, error) { - return renderEcharts(options, "svg") -} diff --git a/echarts_test.go b/echarts_test.go deleted file mode 100644 index 05c2a40..0000000 --- a/echarts_test.go +++ /dev/null @@ -1,592 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "encoding/json" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -func TestEChartsPosition(t *testing.T) { - assert := assert.New(t) - - var p EChartsPosition - err := p.UnmarshalJSON([]byte("12")) - assert.Nil(err) - assert.Equal("12", string(p)) - - err = p.UnmarshalJSON([]byte(`"12%"`)) - assert.Nil(err) - assert.Equal("12%", string(p)) -} -func TestEChartStyle(t *testing.T) { - assert := assert.New(t) - - s := EChartStyle{ - Color: "#aaa", - } - r := drawing.Color{ - R: 170, - G: 170, - B: 170, - A: 255, - } - assert.Equal(chart.Style{ - FillColor: r, - FontColor: r, - StrokeColor: r, - }, s.ToStyle()) -} - -func TestEChartsXAxis(t *testing.T) { - assert := assert.New(t) - ex := EChartsXAxis{} - err := ex.UnmarshalJSON([]byte(`{ - "boundaryGap": false, - "splitNumber": 5, - "data": [ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun" - ] - }`)) - assert.Nil(err) - assert.Equal(EChartsXAxis{ - Data: []EChartsXAxisData{ - { - BoundaryGap: FalseFlag(), - SplitNumber: 5, - Data: []string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }, - }, - }, - }, ex) -} - -func TestEChartsYAxis(t *testing.T) { - assert := assert.New(t) - ey := EChartsYAxis{} - - err := ey.UnmarshalJSON([]byte(`{ - "min": 1, - "max": 10, - "axisLabel": { - "formatter": "ab" - } - }`)) - assert.Nil(err) - assert.Equal(EChartsYAxis{ - Data: []EChartsYAxisData{ - { - Min: NewFloatPoint(1), - Max: NewFloatPoint(10), - AxisLabel: EChartsAxisLabel{ - Formatter: "ab", - }, - }, - }, - }, ey) - - ey = EChartsYAxis{} - err = ey.UnmarshalJSON([]byte(`[ - { - "min": 1, - "max": 10, - "axisLabel": { - "formatter": "ab" - } - }, - { - "min": 2, - "max": 20, - "axisLabel": { - "formatter": "cd" - } - } - ]`)) - assert.Nil(err) - assert.Equal(EChartsYAxis{ - Data: []EChartsYAxisData{ - { - Min: NewFloatPoint(1), - Max: NewFloatPoint(10), - AxisLabel: EChartsAxisLabel{ - Formatter: "ab", - }, - }, - { - Min: NewFloatPoint(2), - Max: NewFloatPoint(20), - AxisLabel: EChartsAxisLabel{ - Formatter: "cd", - }, - }, - }, - }, ey) -} - -func TestEChartsPadding(t *testing.T) { - assert := assert.New(t) - - ep := EChartsPadding{} - - err := ep.UnmarshalJSON([]byte(`10`)) - assert.Nil(err) - assert.Equal(EChartsPadding{ - Box: chart.Box{ - Top: 10, - Right: 10, - Bottom: 10, - Left: 10, - }, - }, ep) - - ep = EChartsPadding{} - err = ep.UnmarshalJSON([]byte(`[10, 20]`)) - assert.Nil(err) - assert.Equal(EChartsPadding{ - Box: chart.Box{ - Top: 10, - Right: 20, - Bottom: 10, - Left: 20, - }, - }, ep) - - ep = EChartsPadding{} - err = ep.UnmarshalJSON([]byte(`[10, 20, 30]`)) - assert.Nil(err) - assert.Equal(EChartsPadding{ - Box: chart.Box{ - Top: 10, - Right: 20, - Bottom: 30, - Left: 20, - }, - }, ep) - - ep = EChartsPadding{} - err = ep.UnmarshalJSON([]byte(`[10, 20, 30, 40]`)) - assert.Nil(err) - assert.Equal(EChartsPadding{ - Box: chart.Box{ - Top: 10, - Right: 20, - Bottom: 30, - Left: 40, - }, - }, ep) - -} -func TestEChartsLegend(t *testing.T) { - assert := assert.New(t) - - el := EChartsLegend{} - - err := json.Unmarshal([]byte(`{ - "data": ["a", "b", "c"], - "align": "right", - "padding": [10], - "left": "20%", - "top": 10 - }`), &el) - assert.Nil(err) - assert.Equal(EChartsLegend{ - Data: []string{ - "a", - "b", - "c", - }, - Align: "right", - Padding: EChartsPadding{ - Box: chart.Box{ - Left: 10, - Top: 10, - Right: 10, - Bottom: 10, - }, - }, - Left: EChartsPosition("20%"), - Top: EChartsPosition("10"), - }, el) -} - -func TestEChartsSeriesData(t *testing.T) { - assert := assert.New(t) - - esd := EChartsSeriesData{} - err := esd.UnmarshalJSON([]byte(`123`)) - assert.Nil(err) - assert.Equal(EChartsSeriesData{ - Value: NewEChartsSeriesDataValue(123), - }, esd) - - esd = EChartsSeriesData{} - err = esd.UnmarshalJSON([]byte(`2.1`)) - assert.Nil(err) - assert.Equal(EChartsSeriesData{ - Value: NewEChartsSeriesDataValue(2.1), - }, esd) - - esd = EChartsSeriesData{} - err = esd.UnmarshalJSON([]byte(`{ - "value": 123.12, - "name": "test", - "itemStyle": { - "color": "#aaa" - } - }`)) - assert.Nil(err) - assert.Equal(EChartsSeriesData{ - Value: NewEChartsSeriesDataValue(123.12), - Name: "test", - ItemStyle: EChartStyle{ - Color: "#aaa", - }, - }, esd) -} - -func TestEChartsSeries(t *testing.T) { - assert := assert.New(t) - - esList := make([]EChartsSeries, 0) - err := json.Unmarshal([]byte(`[ - { - "name": "Email", - "data": [ - 120, - 132 - ] - }, - { - "name": "Union Ads", - "type": "bar", - "data": [ - 220, - 182 - ] - } - ]`), &esList) - assert.Nil(err) - assert.Equal([]EChartsSeries{ - { - Name: "Email", - Data: []EChartsSeriesData{ - { - Value: NewEChartsSeriesDataValue(120), - }, - { - Value: NewEChartsSeriesDataValue(132), - }, - }, - }, - { - Name: "Union Ads", - Type: "bar", - Data: []EChartsSeriesData{ - { - Value: NewEChartsSeriesDataValue(220), - }, - { - Value: NewEChartsSeriesDataValue(182), - }, - }, - }, - }, esList) -} - -func TestEChartsMarkData(t *testing.T) { - assert := assert.New(t) - - emd := EChartsMarkData{} - err := emd.UnmarshalJSON([]byte(`{"type": "average"}`)) - assert.Nil(err) - assert.Equal("average", emd.Type) - - emd = EChartsMarkData{} - err = emd.UnmarshalJSON([]byte(`[{}, {"type": "average"}]`)) - assert.Nil(err) - assert.Equal("average", emd.Type) -} - -func TestEChartsMarkPoint(t *testing.T) { - assert := assert.New(t) - - p := EChartsMarkPoint{} - - err := json.Unmarshal([]byte(`{ - "symbolSize": 30, - "data": [ - { - "type": "max" - }, - { - "type": "min" - } - ] - }`), &p) - assert.Nil(err) - assert.Equal(SeriesMarkPoint{ - SymbolSize: 30, - Data: []SeriesMarkData{ - { - Type: "max", - }, - { - Type: "min", - }, - }, - }, p.ToSeriesMarkPoint()) -} - -func TestEChartsMarkLine(t *testing.T) { - assert := assert.New(t) - l := EChartsMarkLine{} - - err := json.Unmarshal([]byte(`{ - "data": [ - { - "type": "max" - }, - { - "type": "min" - } - ] - }`), &l) - assert.Nil(err) - assert.Equal(SeriesMarkLine{ - Data: []SeriesMarkData{ - { - Type: "max", - }, - { - Type: "min", - }, - }, - }, l.ToSeriesMarkLine()) -} - -func TestEChartsTextStyle(t *testing.T) { - assert := assert.New(t) - - s := EChartsTextStyle{ - Color: "#aaa", - FontFamily: "test", - FontSize: 14, - } - assert.Equal(chart.Style{ - FontColor: drawing.Color{ - R: 170, - G: 170, - B: 170, - A: 255, - }, - FontSize: 14, - }, s.ToStyle()) -} - -func TestEChartsSeriesList(t *testing.T) { - assert := assert.New(t) - - // pie - es := EChartsSeriesList{ - { - Type: ChartTypePie, - Radius: "30%", - Data: []EChartsSeriesData{ - { - Name: "1", - Value: EChartsSeriesDataValue{ - values: []float64{ - 1, - }, - }, - }, - { - Name: "2", - Value: EChartsSeriesDataValue{ - values: []float64{ - 2, - }, - }, - }, - }, - }, - } - assert.Equal(SeriesList{ - { - Type: ChartTypePie, - Name: "1", - Label: SeriesLabel{ - Show: true, - }, - Radius: "30%", - Data: []SeriesData{ - { - Value: 1, - }, - }, - }, - { - Type: ChartTypePie, - Name: "2", - Label: SeriesLabel{ - Show: true, - }, - Radius: "30%", - Data: []SeriesData{ - { - Value: 2, - }, - }, - }, - }, es.ToSeriesList()) - - es = EChartsSeriesList{ - { - Type: ChartTypeBar, - Data: []EChartsSeriesData{ - { - Value: NewEChartsSeriesDataValue(1), - ItemStyle: EChartStyle{ - Color: "#aaa", - }, - }, - { - Value: NewEChartsSeriesDataValue(2), - }, - }, - YAxisIndex: 1, - }, - { - Data: []EChartsSeriesData{ - { - Value: NewEChartsSeriesDataValue(3), - }, - { - Value: NewEChartsSeriesDataValue(4), - }, - }, - ItemStyle: EChartStyle{ - Color: "#ccc", - }, - Label: EChartsLabelOption{ - Color: "#ddd", - Show: true, - Distance: 5, - }, - }, - } - assert.Equal(SeriesList{ - { - Type: ChartTypeBar, - Data: []SeriesData{ - { - Value: 1, - Style: chart.Style{ - FontColor: drawing.Color{ - R: 170, - G: 170, - B: 170, - A: 255, - }, - StrokeColor: drawing.Color{ - R: 170, - G: 170, - B: 170, - A: 255, - }, - FillColor: drawing.Color{ - R: 170, - G: 170, - B: 170, - A: 255, - }, - }, - }, - { - Value: 2, - }, - }, - YAxisIndex: 1, - }, - { - Data: []SeriesData{ - { - Value: 3, - }, - { - Value: 4, - }, - }, - Style: chart.Style{ - FontColor: drawing.Color{ - R: 204, - G: 204, - B: 204, - A: 255, - }, - StrokeColor: drawing.Color{ - R: 204, - G: 204, - B: 204, - A: 255, - }, - FillColor: drawing.Color{ - R: 204, - G: 204, - B: 204, - A: 255, - }, - }, - Label: SeriesLabel{ - Color: drawing.Color{ - R: 221, - G: 221, - B: 221, - A: 255, - }, - Show: true, - Distance: 5, - }, - MarkPoint: SeriesMarkPoint{}, - MarkLine: SeriesMarkLine{}, - }, - }, es.ToSeriesList()) - -} diff --git a/examples/basic/main.go b/examples/basic/main.go deleted file mode 100644 index 1e7af8d..0000000 --- a/examples/basic/main.go +++ /dev/null @@ -1,94 +0,0 @@ -package main - -import ( - "io/ioutil" - "os" - "path/filepath" - - charts "github.com/vicanso/go-charts" -) - -func writeFile(file string, buf []byte) error { - tmpPath := "./tmp" - err := os.MkdirAll(tmpPath, 0700) - if err != nil { - return err - } - - file = filepath.Join(tmpPath, file) - err = ioutil.WriteFile(file, buf, 0600) - if err != nil { - return err - } - return nil -} - -func chartsRender() ([]byte, error) { - d, err := charts.LineRender([][]float64{ - { - 150, - 230, - 224, - 218, - 135, - 147, - 260, - }, - }, - // output type - charts.PNGTypeOption(), - // title - charts.TitleOptionFunc(charts.TitleOption{ - Text: "Line", - }), - // x axis - charts.XAxisOptionFunc(charts.NewXAxisOption([]string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - })), - ) - if err != nil { - return nil, err - } - return d.Bytes() -} - -func echartsRender() ([]byte, error) { - return charts.RenderEChartsToPNG(`{ - "title": { - "text": "Line" - }, - "xAxis": { - "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] - }, - "series": [ - { - "data": [150, 230, 224, 218, 135, 147, 260] - } - ] - }`) -} - -type Render func() ([]byte, error) - -func main() { - m := map[string]Render{ - "charts-line.png": chartsRender, - "echarts-line.png": echartsRender, - } - for name, fn := range m { - buf, err := fn() - if err != nil { - panic(err) - } - err = writeFile(name, buf) - if err != nil { - panic(err) - } - } -} diff --git a/examples/charts/main.go b/examples/charts/main.go deleted file mode 100644 index fddbe6d..0000000 --- a/examples/charts/main.go +++ /dev/null @@ -1,1809 +0,0 @@ -package main - -import ( - "bytes" - "net/http" - "strconv" - - charts "github.com/vicanso/go-charts" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -var html = ` - - - - - - - go-charts - - -
{{body}}
- - -` - -func handler(w http.ResponseWriter, req *http.Request, chartOptions []charts.ChartOption, echartsOptions []string) { - if req.URL.Path != "/" && - req.URL.Path != "/echarts" { - return - } - query := req.URL.Query() - theme := query.Get("theme") - width, _ := strconv.Atoi(query.Get("width")) - height, _ := strconv.Atoi(query.Get("height")) - charts.SetDefaultWidth(width) - charts.SetDefaultWidth(height) - bytesList := make([][]byte, 0) - for _, opt := range chartOptions { - opt.Theme = theme - d, err := charts.Render(opt) - if err != nil { - panic(err) - } - buf, err := d.Bytes() - if err != nil { - panic(err) - } - bytesList = append(bytesList, buf) - } - for _, opt := range echartsOptions { - buf, err := charts.RenderEChartsToSVG(opt) - 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) -} - -func indexHandler(w http.ResponseWriter, req *http.Request) { - chartOptions := []charts.ChartOption{ - // 普通折线图 - { - Title: charts.TitleOption{ - Text: "Line", - }, - Legend: charts.NewLegendOption([]string{ - "Email", - "Union Ads", - "Video Ads", - "Direct", - "Search Engine", - }), - XAxis: charts.NewXAxisOption([]string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }), - SeriesList: []charts.Series{ - charts.NewSeriesFromValues([]float64{ - 120, - 132, - 101, - 134, - 90, - 230, - 210, - }), - charts.NewSeriesFromValues([]float64{ - 220, - 182, - 191, - 234, - 290, - 330, - 310, - }), - charts.NewSeriesFromValues([]float64{ - 150, - 232, - 201, - 154, - 190, - 330, - 410, - }), - charts.NewSeriesFromValues([]float64{ - 320, - 332, - 301, - 334, - 390, - 330, - 320, - }), - charts.NewSeriesFromValues([]float64{ - 820, - 932, - 901, - 934, - 1290, - 1330, - 1320, - }), - }, - }, - // 温度折线图 - { - Title: charts.TitleOption{ - Text: "Temperature Change in the Coming Week", - }, - Padding: chart.Box{ - Top: 20, - Left: 20, - Right: 30, - Bottom: 20, - }, - Legend: charts.NewLegendOption([]string{ - "Highest", - "Lowest", - }, charts.PositionRight), - XAxis: charts.NewXAxisOption([]string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }, charts.FalseFlag()), - SeriesList: []charts.Series{ - { - Data: charts.NewSeriesDataFromValues([]float64{ - 14, - 11, - 13, - 11, - 12, - 12, - 7, - }), - MarkPoint: charts.NewMarkPoint(charts.SeriesMarkDataTypeMax, charts.SeriesMarkDataTypeMin), - MarkLine: charts.NewMarkLine(charts.SeriesMarkDataTypeAverage), - }, - { - Data: charts.NewSeriesDataFromValues([]float64{ - 1, - -2, - 2, - 5, - 3, - 2, - 0, - }), - MarkLine: charts.NewMarkLine(charts.SeriesMarkDataTypeAverage), - }, - }, - }, - // 柱状图 - { - Title: charts.TitleOption{ - Text: "Bar", - }, - XAxis: charts.NewXAxisOption([]string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }), - Legend: charts.LegendOption{ - Data: []string{ - "Rainfall", - "Evaporation", - }, - Icon: charts.LegendIconRect, - }, - SeriesList: []charts.Series{ - charts.NewSeriesFromValues([]float64{ - 120, - 200, - 150, - 80, - 70, - 110, - 130, - }, charts.ChartTypeBar), - { - Type: charts.ChartTypeBar, - Data: []charts.SeriesData{ - { - Value: 100, - }, - { - Value: 190, - Style: chart.Style{ - FillColor: drawing.Color{ - R: 169, - G: 0, - B: 0, - A: 255, - }, - }, - }, - { - Value: 230, - }, - { - Value: 140, - }, - { - Value: 100, - }, - { - Value: 200, - }, - { - Value: 180, - }, - }, - }, - }, - }, - // 柱状图+mark - { - Title: charts.TitleOption{ - Text: "Rainfall vs Evaporation", - Subtext: "Fake Data", - }, - Padding: chart.Box{ - Top: 20, - Right: 20, - Bottom: 20, - Left: 20, - }, - XAxis: charts.NewXAxisOption([]string{ - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec", - }), - Legend: charts.NewLegendOption([]string{ - "Rainfall", - "Evaporation", - }, charts.PositionRight), - SeriesList: []charts.Series{ - { - Type: charts.ChartTypeBar, - Data: charts.NewSeriesDataFromValues([]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, - }), - MarkPoint: charts.NewMarkPoint( - charts.SeriesMarkDataTypeMax, - charts.SeriesMarkDataTypeMin, - ), - MarkLine: charts.NewMarkLine( - charts.SeriesMarkDataTypeAverage, - ), - }, - { - Type: charts.ChartTypeBar, - Data: charts.NewSeriesDataFromValues([]float64{ - 2.6, - 5.9, - 9.0, - 26.4, - 28.7, - 70.7, - 175.6, - 182.2, - 48.7, - 18.8, - 6.0, - 2.3, - }), - MarkPoint: charts.NewMarkPoint( - charts.SeriesMarkDataTypeMax, - charts.SeriesMarkDataTypeMin, - ), - MarkLine: charts.NewMarkLine( - charts.SeriesMarkDataTypeAverage, - ), - }, - }, - }, - // 双Y轴示例 - { - XAxis: charts.NewXAxisOption([]string{ - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec", - }), - Legend: charts.NewLegendOption([]string{ - "Evaporation", - "Precipitation", - "Temperature", - }), - YAxisList: []charts.YAxisOption{ - { - Formatter: "{value}°C", - Color: drawing.Color{ - R: 250, - G: 200, - B: 88, - A: 255, - }, - }, - { - Formatter: "{value}ml", - Color: drawing.Color{ - R: 84, - G: 112, - B: 198, - A: 255, - }, - }, - }, - SeriesList: []charts.Series{ - { - Type: charts.ChartTypeBar, - Data: charts.NewSeriesDataFromValues([]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, - 10.2, - }), - YAxisIndex: 1, - }, - { - Type: charts.ChartTypeBar, - Data: charts.NewSeriesDataFromValues([]float64{ - 2.6, - 5.9, - 9.0, - 26.4, - 28.7, - 70.7, - 175.6, - 182.2, - 48.7, - 18.8, - 6.0, - 2.3, - 20.2, - }), - YAxisIndex: 1, - }, - { - Data: charts.NewSeriesDataFromValues([]float64{ - 2.0, - 2.2, - 3.3, - 4.5, - 6.3, - 10.2, - 20.3, - 23.4, - 23.0, - 16.5, - 12.0, - 6.2, - 30.3, - }), - }, - }, - }, - // 饼图 - { - Title: charts.TitleOption{ - Text: "Referer of a Website", - Subtext: "Fake Data", - Left: charts.PositionCenter, - }, - Legend: charts.LegendOption{ - Orient: charts.OrientVertical, - Data: []string{ - "Search Engine", - "Direct", - "Email", - "Union Ads", - "Video Ads", - }, - Left: charts.PositionLeft, - }, - SeriesList: charts.NewPieSeriesList([]float64{ - 1048, - 735, - 580, - 484, - 300, - }, charts.PieSeriesOption{ - Label: charts.SeriesLabel{ - Show: true, - }, - Radius: "35%", - }), - }, - // 雷达图 - { - Title: charts.TitleOption{ - Text: "Basic Radar Chart", - }, - Legend: charts.NewLegendOption([]string{ - "Allocated Budget", - "Actual Spending", - }), - RadarIndicators: []charts.RadarIndicator{ - { - Name: "Sales", - Max: 6500, - }, - { - Name: "Administration", - Max: 16000, - }, - { - Name: "Information Technology", - Max: 30000, - }, - { - Name: "Customer Support", - Max: 38000, - }, - { - Name: "Development", - Max: 52000, - }, - { - Name: "Marketing", - Max: 25000, - }, - }, - SeriesList: charts.SeriesList{ - { - Type: charts.ChartTypeRadar, - Data: charts.NewSeriesDataFromValues([]float64{ - 4200, - 3000, - 20000, - 35000, - 50000, - 18000, - }), - }, - { - Type: charts.ChartTypeRadar, - Data: charts.NewSeriesDataFromValues([]float64{ - 5000, - 14000, - 28000, - 26000, - 42000, - 21000, - }), - }, - }, - }, - // 漏斗图 - { - Title: charts.TitleOption{ - Text: "Funnel", - }, - Legend: charts.NewLegendOption([]string{ - "Show", - "Click", - "Visit", - "Inquiry", - "Order", - }), - SeriesList: []charts.Series{ - { - Type: charts.ChartTypeFunnel, - Name: "Visit", - Data: charts.NewSeriesDataFromValues([]float64{ - 60, - }), - }, - { - Type: charts.ChartTypeFunnel, - Name: "Inquiry", - Data: charts.NewSeriesDataFromValues([]float64{ - 40, - }), - }, - { - Type: charts.ChartTypeFunnel, - Name: "Order", - Data: charts.NewSeriesDataFromValues([]float64{ - 20, - }), - }, - { - Type: charts.ChartTypeFunnel, - Name: "Click", - Data: charts.NewSeriesDataFromValues([]float64{ - 80, - }), - }, - { - Type: charts.ChartTypeFunnel, - Name: "Show", - Data: charts.NewSeriesDataFromValues([]float64{ - 100, - }), - }, - }, - }, - // 多图展示 - { - Legend: charts.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: charts.NewXAxisOption([]string{ - "2012", - "2013", - "2014", - "2015", - "2016", - "2017", - }), - YAxisList: []charts.YAxisOption{ - { - - Min: charts.NewFloatPoint(0), - Max: charts.NewFloatPoint(90), - }, - }, - SeriesList: []charts.Series{ - charts.NewSeriesFromValues([]float64{ - 56.5, - 82.1, - 88.7, - 70.1, - 53.4, - 85.1, - }), - charts.NewSeriesFromValues([]float64{ - 51.1, - 51.4, - 55.1, - 53.3, - 73.8, - 68.7, - }), - charts.NewSeriesFromValues([]float64{ - 40.1, - 62.2, - 69.5, - 36.4, - 45.2, - 32.5, - }, charts.ChartTypeBar), - charts.NewSeriesFromValues([]float64{ - 25.2, - 37.1, - 41.2, - 18, - 33.9, - 49.1, - }, charts.ChartTypeBar), - }, - Children: []charts.ChartOption{ - { - Legend: charts.LegendOption{ - Show: charts.FalseFlag(), - Data: []string{ - "Milk Tea", - "Matcha Latte", - "Cheese Cocoa", - "Walnut Brownie", - }, - }, - Box: chart.Box{ - Top: 20, - Left: 400, - Right: 500, - Bottom: 120, - }, - SeriesList: charts.NewPieSeriesList([]float64{ - 435.9, - 354.3, - 285.9, - 204.5, - }, charts.PieSeriesOption{ - Label: charts.SeriesLabel{ - Show: true, - }, - Radius: "35%", - }), - }, - }, - }, - } - handler(w, req, chartOptions, nil) -} - -func echartsHandler(w http.ResponseWriter, req *http.Request) { - echartsOptions := []string{ - `{ - "xAxis": { - "type": "category", - "data": [ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun" - ] - }, - "yAxis": { - "type": "value" - }, - "series": [ - { - "data": [ - 150, - 230, - 224, - 218, - 135, - 147, - 260 - ], - "type": "line" - } - ] - }`, - `{ - "title": { - "text": "Multiple Line" - }, - "tooltip": { - "trigger": "axis" - }, - "legend": { - "left": "right", - "data": [ - "Email", - "Union Ads", - "Video Ads", - "Direct", - "Search Engine" - ] - }, - "grid": { - "left": "3%", - "right": "4%", - "bottom": "3%", - "containLabel": true - }, - "toolbox": { - "feature": { - "saveAsImage": {} - } - }, - "xAxis": { - "type": "category", - "boundaryGap": false, - "data": [ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun" - ] - }, - "yAxis": { - "type": "value" - }, - "series": [ - { - "name": "Email", - "type": "line", - "data": [ - 120, - 132, - 101, - 134, - 90, - 230, - 210 - ] - }, - { - "name": "Union Ads", - "type": "line", - "data": [ - 220, - 182, - 191, - 234, - 290, - 330, - 310 - ] - }, - { - "name": "Video Ads", - "type": "line", - "data": [ - 150, - 232, - 201, - 154, - 190, - 330, - 410 - ] - }, - { - "name": "Direct", - "type": "line", - "data": [ - 320, - 332, - 301, - 334, - 390, - 330, - 320 - ] - }, - { - "name": "Search Engine", - "type": "line", - "data": [ - 820, - 932, - 901, - 934, - 1290, - 1330, - 1320 - ] - } - ] - }`, - `{ - "title": { - "text": "Temperature Change in the Coming Week" - }, - "legend": { - "left": "right" - }, - "padding": [10, 30, 10, 10], - "xAxis": { - "type": "category", - "boundaryGap": false, - "data": [ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun" - ] - }, - "yAxis": { - "axisLabel": { - "formatter": "{value} °C" - } - }, - "series": [ - { - "name": "Highest", - "type": "line", - "data": [ - 10, - 11, - 13, - 11, - 12, - 12, - 9 - ], - "markPoint": { - "data": [ - { - "type": "max" - }, - { - "type": "min" - } - ] - }, - "markLine": { - "data": [ - { - "type": "average" - } - ] - } - }, - { - "name": "Lowest", - "type": "line", - "data": [ - 1, - -2, - 2, - 5, - 3, - 2, - 0 - ], - "markPoint": { - "data": [ - { - "type": "min" - } - ] - }, - "markLine": { - "data": [ - { - "type": "average" - }, - { - "type": "max" - } - ] - } - } - ] - }`, - `{ - "xAxis": { - "type": "category", - "data": [ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun" - ] - }, - "yAxis": { - "type": "value" - }, - "series": [ - { - "data": [ - 120, - 200, - 150, - 80, - 70, - 110, - 130 - ], - "type": "bar" - } - ] - }`, - `{ - "xAxis": { - "type": "category", - "data": [ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun" - ] - }, - "yAxis": { - "type": "value" - }, - "series": [ - { - "data": [ - 120, - { - "value": 200, - "itemStyle": { - "color": "#a90000" - } - }, - 150, - 80, - 70, - 110, - 130 - ], - "type": "bar" - } - ] - }`, - `{ - "title": { - "text": "Rainfall vs Evaporation", - "subtext": "Fake Data" - }, - "legend": { - "data": [ - "Rainfall", - "Evaporation" - ] - }, - "padding": [10, 30, 10, 10], - "xAxis": [ - { - "type": "category", - "data": [ - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec" - ] - } - ], - "series": [ - { - "name": "Rainfall", - "type": "bar", - "data": [ - 2, - 4.9, - 7, - 23.2, - 25.6, - 76.7, - 135.6, - 162.2, - 32.6, - 20, - 6.4, - 3.3 - ], - "markPoint": { - "data": [ - { - "type": "max" - }, - { - "type": "min" - } - ] - }, - "markLine": { - "data": [ - { - "type": "average" - } - ] - } - }, - { - "name": "Evaporation", - "type": "bar", - "data": [ - 2.6, - 5.9, - 9, - 26.4, - 28.7, - 70.7, - 175.6, - 182.2, - 48.7, - 18.8, - 6, - 2.3 - ], - "markPoint": { - "data": [ - { - "type": "max" - }, - { - "type": "min" - } - ] - }, - "markLine": { - "data": [ - { - "type": "average" - } - ] - } - } - ] - }`, - `{ - "legend": { - "data": [ - "Evaporation", - "Precipitation", - "Temperature" - ] - }, - "xAxis": [ - { - "type": "category", - "data": [ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun" - ], - "axisPointer": { - "type": "shadow" - } - } - ], - "yAxis": [ - { - "type": "value", - "name": "Precipitation", - "min": 0, - "max": 240, - "axisLabel": { - "formatter": "{value} ml" - } - }, - { - "type": "value", - "name": "Temperature", - "min": 0, - "max": 24, - "axisLabel": { - "formatter": "{value} °C" - } - } - ], - "series": [ - { - "name": "Evaporation", - "type": "bar", - "tooltip": {}, - "data": [ - 2, - 4.9, - 7, - 23.2, - 25.6, - 76.7, - 135.6, - 162.2, - 32.6, - 20, - 6.4, - 3.3 - ] - }, - { - "name": "Precipitation", - "type": "bar", - "tooltip": {}, - "data": [ - 2.6, - 5.9, - 9, - 26.4, - 28.7, - 70.7, - 175.6, - 182.2, - 48.7, - 18.8, - 6, - 2.3 - ] - }, - { - "name": "Temperature", - "type": "line", - "yAxisIndex": 1, - "tooltip": {}, - "data": [ - 2, - 2.2, - 3.3, - 4.5, - 6.3, - 10.2, - 20.3, - 23.4, - 23, - 16.5, - 12, - 6.2 - ] - } - ] - }`, - `{ - "tooltip": { - "trigger": "axis", - "axisPointer": { - "type": "cross" - } - }, - "grid": { - "right": "20%" - }, - "toolbox": { - "feature": { - "dataView": { - "show": true, - "readOnly": false - }, - "restore": { - "show": true - }, - "saveAsImage": { - "show": true - } - } - }, - "legend": { - "data": [ - "Evaporation", - "Precipitation", - "Temperature" - ] - }, - "xAxis": [ - { - "type": "category", - "axisTick": { - "alignWithLabel": true - }, - "data": [ - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec" - ] - } - ], - "yAxis": [ - { - "type": "value", - "name": "温度", - "position": "left", - "alignTicks": true, - "axisLine": { - "show": true, - "lineStyle": { - "color": "#EE6666" - } - }, - "axisLabel": { - "formatter": "{value} °C" - } - }, - { - "type": "value", - "name": "Evaporation", - "position": "right", - "alignTicks": true, - "axisLine": { - "show": true, - "lineStyle": { - "color": "#5470C6" - } - }, - "axisLabel": { - "formatter": "{value} ml" - } - } - ], - "series": [ - { - "name": "Evaporation", - "type": "bar", - "yAxisIndex": 1, - "data": [ - 2, - 4.9, - 7, - 23.2, - 25.6, - 76.7, - 135.6, - 162.2, - 32.6, - 20, - 6.4, - 3.3 - ] - }, - { - "name": "Precipitation", - "type": "bar", - "yAxisIndex": 1, - "data": [ - 2.6, - 5.9, - 9, - 26.4, - 28.7, - 70.7, - 175.6, - 182.2, - 48.7, - 18.8, - 6, - 2.3 - ] - }, - { - "name": "Temperature", - "type": "line", - "data": [ - 2, - 2.2, - 3.3, - 4.5, - 6.3, - 10.2, - 20.3, - 23.4, - 23, - 16.5, - 12, - 6.2 - ] - } - ] - }`, - `{ - "title": { - "text": "Referer of a Website", - "subtext": "Fake Data", - "left": "center" - }, - "tooltip": { - "trigger": "item" - }, - "legend": { - "orient": "vertical", - "left": "left" - }, - "series": [ - { - "name": "Access From", - "type": "pie", - "radius": "50%", - "data": [ - { - "value": 1048, - "name": "Search Engine" - }, - { - "value": 735, - "name": "Direct" - }, - { - "value": 580, - "name": "Email" - }, - { - "value": 484, - "name": "Union Ads" - }, - { - "value": 300, - "name": "Video Ads" - } - ] - } - ] - }`, - `{ - "title": { - "text": "Rainfall" - }, - "padding": [10, 10, 10, 30], - "legend": { - "data": [ - "GZ", - "SH" - ] - }, - "xAxis": { - "type": "category", - "splitNumber": 6, - "data": [ - "01-01", - "01-02", - "01-03", - "01-04", - "01-05", - "01-06", - "01-07", - "01-08", - "01-09", - "01-10", - "01-11", - "01-12", - "01-13", - "01-14", - "01-15", - "01-16", - "01-17", - "01-18", - "01-19", - "01-20", - "01-21", - "01-22", - "01-23", - "01-24", - "01-25", - "01-26", - "01-27", - "01-28", - "01-29", - "01-30", - "01-31" - ] - }, - "yAxis": { - "axisLabel": { - "formatter": "{value} mm" - } - }, - "series": [ - { - "type": "bar", - "data": [ - 928, - 821, - 889, - 600, - 547, - 783, - 197, - 853, - 430, - 346, - 63, - 465, - 309, - 334, - 141, - 538, - 792, - 58, - 922, - 807, - 298, - 243, - 744, - 885, - 812, - 231, - 330, - 220, - 984, - 221, - 429 - ] - }, - { - "type": "bar", - "data": [ - 749, - 201, - 296, - 579, - 255, - 159, - 902, - 246, - 149, - 158, - 507, - 776, - 186, - 79, - 390, - 222, - 601, - 367, - 221, - 411, - 714, - 620, - 966, - 73, - 203, - 631, - 833, - 610, - 487, - 677, - 596 - ] - } - ] - }`, - `{ - "title": { - "text": "Basic Radar Chart" - }, - "legend": { - "data": [ - "Allocated Budget", - "Actual Spending" - ] - }, - "radar": { - "indicator": [ - { - "name": "Sales", - "max": 6500 - }, - { - "name": "Administration", - "max": 16000 - }, - { - "name": "Information Technology", - "max": 30000 - }, - { - "name": "Customer Support", - "max": 38000 - }, - { - "name": "Development", - "max": 52000 - }, - { - "name": "Marketing", - "max": 25000 - } - ] - }, - "series": [ - { - "name": "Budget vs spending", - "type": "radar", - "data": [ - { - "value": [ - 4200, - 3000, - 20000, - 35000, - 50000, - 18000 - ], - "name": "Allocated Budget" - }, - { - "value": [ - 5000, - 14000, - 28000, - 26000, - 42000, - 21000 - ], - "name": "Actual Spending" - } - ] - } - ] - }`, - `{ - "title": { - "text": "Funnel" - }, - "tooltip": { - "trigger": "item", - "formatter": "{a}
{b} : {c}%" - }, - "toolbox": { - "feature": { - "dataView": { - "readOnly": false - }, - "restore": {}, - "saveAsImage": {} - } - }, - "legend": { - "data": [ - "Show", - "Click", - "Visit", - "Inquiry", - "Order" - ] - }, - "series": [ - { - "name": "Funnel", - "type": "funnel", - "left": "10%", - "top": 60, - "bottom": 60, - "width": "80%", - "min": 0, - "max": 100, - "minSize": "0%", - "maxSize": "100%", - "sort": "descending", - "gap": 2, - "label": { - "show": true, - "position": "inside" - }, - "labelLine": { - "length": 10, - "lineStyle": { - "width": 1, - "type": "solid" - } - }, - "itemStyle": { - "borderColor": "#fff", - "borderWidth": 1 - }, - "emphasis": { - "label": { - "fontSize": 20 - } - }, - "data": [ - { - "value": 60, - "name": "Visit" - }, - { - "value": 40, - "name": "Inquiry" - }, - { - "value": 20, - "name": "Order" - }, - { - "value": 80, - "name": "Click" - }, - { - "value": 100, - "name": "Show" - } - ] - } - ] - }`, - `{ - "legend": { - "top": "-140", - "data": [ - "Milk Tea", - "Matcha Latte", - "Cheese Cocoa", - "Walnut Brownie" - ] - }, - "padding": [ - 150, - 10, - 10, - 10 - ], - "xAxis": [ - { - "data": [ - "2012", - "2013", - "2014", - "2015", - "2016", - "2017" - ] - } - ], - "series": [ - { - "data": [ - 56.5, - 82.1, - 88.7, - 70.1, - 53.4, - 85.1 - ] - }, - { - "data": [ - 51.1, - 51.4, - 55.1, - 53.3, - 73.8, - 68.7 - ] - }, - { - "data": [ - 40.1, - 62.2, - 69.5, - 36.4, - 45.2, - 32.5 - ] - }, - { - "data": [ - 25.2, - 37.1, - 41.2, - 18, - 33.9, - 49.1 - ] - } - ], - "children": [ - { - "box": { - "left": 0, - "top": 30, - "right": 600, - "bottom": 150 - }, - "legend": { - "show": false - }, - "series": [ - { - "type": "pie", - "radius": "50%", - "data": [ - { - "value": 435.9, - "name": "Milk Tea" - }, - { - "value": 354.3, - "name": "Matcha Latte" - }, - { - "value": 285.9, - "name": "Cheese Cocoa" - }, - { - "value": 204.5, - "name": "Walnut Brownie" - } - ] - } - ] - } - ] - }`, - } - handler(w, req, nil, echartsOptions) -} - -func main() { - http.HandleFunc("/", indexHandler) - http.HandleFunc("/echarts", echartsHandler) - http.ListenAndServe(":3012", nil) -} diff --git a/funnel.go b/funnel.go deleted file mode 100644 index f083306..0000000 --- a/funnel.go +++ /dev/null @@ -1,141 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "fmt" - "sort" - - "github.com/dustin/go-humanize" - "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" -) - -type funnelChartOption struct { - Theme string - Font *truetype.Font - SeriesList SeriesList -} - -func funnelChartRender(opt funnelChartOption, result *basicRenderResult) error { - d, err := NewDraw(DrawOption{ - Parent: result.d, - }, PaddingOption(chart.Box{ - Top: result.titleBox.Height(), - })) - if err != nil { - return err - } - seriesList := make([]Series, len(opt.SeriesList)) - copy(seriesList, opt.SeriesList) - sort.Slice(seriesList, func(i, j int) bool { - // 大的数据在前 - return seriesList[i].Data[0].Value > seriesList[j].Data[0].Value - }) - max := seriesList[0].Data[0].Value - min := float64(0) - for _, item := range seriesList { - if item.Max != nil { - max = *item.Max - } - if item.Min != nil { - min = *item.Min - } - } - - theme := NewTheme(opt.Theme) - gap := 2 - height := d.Box.Height() - width := d.Box.Width() - count := len(seriesList) - - h := (height - gap*(count-1)) / count - - y := 0 - widthList := make([]int, len(seriesList)) - textList := make([]string, len(seriesList)) - 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) - } - - for index, w := range widthList { - series := seriesList[index] - nextWidth := 0 - if index+1 < len(widthList) { - nextWidth = widthList[index+1] - } - topStartX := (width - w) >> 1 - topEndX := topStartX + w - bottomStartX := (width - nextWidth) >> 1 - bottomEndX := bottomStartX + nextWidth - points := []Point{ - { - X: topStartX, - Y: y, - }, - { - X: topEndX, - Y: y, - }, - { - X: bottomEndX, - Y: y + h, - }, - { - X: bottomStartX, - Y: y + h, - }, - { - X: topStartX, - Y: y, - }, - } - color := theme.GetSeriesColor(series.index) - d.fill(points, chart.Style{ - FillColor: color, - }) - - // 文本 - text := textList[index] - r := d.Render - textStyle := chart.Style{ - FontColor: theme.GetTextColor(), - FontSize: labelFontSize, - Font: opt.Font, - } - textStyle.GetTextOptions().WriteToRenderer(r) - textBox := r.MeasureText(text) - textX := width>>1 - textBox.Width()>>1 - textY := y + h>>1 - d.text(text, textX, textY) - - y += (h + gap) - } - - return nil -} diff --git a/funnel_test.go b/funnel_test.go deleted file mode 100644 index 530fa53..0000000 --- a/funnel_test.go +++ /dev/null @@ -1,91 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2" -) - -func TestFunnelChartRender(t *testing.T) { - assert := assert.New(t) - - d, err := NewDraw(DrawOption{ - Width: 250, - Height: 150, - }) - assert.Nil(err) - f, _ := chart.GetDefaultFont() - err = funnelChartRender(funnelChartOption{ - Font: f, - SeriesList: []Series{ - { - Type: ChartTypeFunnel, - Name: "Visit", - Data: NewSeriesDataFromValues([]float64{ - 60, - }), - }, - { - Type: ChartTypeFunnel, - Name: "Inquiry", - Data: NewSeriesDataFromValues([]float64{ - 40, - }), - index: 1, - }, - { - Type: ChartTypeFunnel, - Name: "Order", - Data: NewSeriesDataFromValues([]float64{ - 20, - }), - index: 2, - }, - { - Type: ChartTypeFunnel, - Name: "Click", - Data: NewSeriesDataFromValues([]float64{ - 80, - }), - index: 3, - }, - { - Type: ChartTypeFunnel, - Name: "Show", - Data: NewSeriesDataFromValues([]float64{ - 100, - }), - index: 4, - }, - }, - }, &basicRenderResult{ - d: d, - }) - assert.Nil(err) - data, err := d.Bytes() - assert.Nil(err) - assert.Equal("\\nShow(100%)Click(80%)Visit(60%)Inquiry(40%)Order(20%)", string(data)) -} diff --git a/legend.go b/legend.go deleted file mode 100644 index df72757..0000000 --- a/legend.go +++ /dev/null @@ -1,226 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "strconv" - "strings" - - "github.com/wcharczuk/go-chart/v2" -) - -type LegendOption struct { - theme string - // Legend show flag, if nil or true, the legend will be shown - Show *bool - // Legend text style - Style chart.Style - // Text array of legend - Data []string - // Distance between legend component and the left side of the container. - // It can be pixel value: 20, percentage value: 20%, - // or position value: right, center. - Left string - // Distance between legend component and the top side of the container. - // It can be pixel value: 20. - Top string - // Legend marker and text aligning, it can be left or right, default is left. - Align string - // The layout orientation of legend, it can be horizontal or vertical, default is horizontal. - Orient string - // Icon of the legend. - Icon string -} - -const ( - LegendIconRect = "rect" -) - -// NewLegendOption creates a new legend option by legend text list -func NewLegendOption(data []string, position ...string) LegendOption { - opt := LegendOption{ - Data: data, - } - if len(position) != 0 { - opt.Left = position[0] - } - return opt -} - -type legend struct { - d *Draw - opt *LegendOption -} - -func NewLegend(d *Draw, opt LegendOption) *legend { - return &legend{ - d: d, - opt: &opt, - } -} - -func (l *legend) Render() (chart.Box, error) { - d := l.d - opt := l.opt - if len(opt.Data) == 0 || isFalse(opt.Show) { - return chart.BoxZero, nil - } - theme := NewTheme(opt.theme) - padding := opt.Style.Padding - legendDraw, err := NewDraw(DrawOption{ - Parent: d, - }, PaddingOption(padding)) - if err != nil { - return chart.BoxZero, err - } - r := legendDraw.Render - opt.Style.GetTextOptions().WriteToRenderer(r) - - x := 0 - y := 0 - top := 0 - // TODO TOP 暂只支持数值 - if opt.Top != "" { - top, _ = strconv.Atoi(opt.Top) - y += top - } - legendWidth := 30 - legendDotHeight := 5 - textPadding := 5 - legendMargin := 10 - // 往下移2倍dot的高度 - y += 2 * legendDotHeight - - widthCount := 0 - maxTextWidth := 0 - // 文本宽度 - for _, text := range opt.Data { - b := r.MeasureText(text) - if b.Width() > maxTextWidth { - maxTextWidth = b.Width() - } - widthCount += b.Width() - } - if opt.Orient == OrientVertical { - widthCount = maxTextWidth + legendWidth + textPadding - } else { - // 加上标记 - widthCount += legendWidth * len(opt.Data) - // 文本的padding - widthCount += 2 * textPadding * len(opt.Data) - // margin的宽度 - widthCount += legendMargin * (len(opt.Data) - 1) - } - - left := 0 - switch opt.Left { - case PositionRight: - left = legendDraw.Box.Width() - widthCount - case PositionCenter: - left = (legendDraw.Box.Width() - widthCount) >> 1 - default: - if strings.HasSuffix(opt.Left, "%") { - value, _ := strconv.Atoi(strings.ReplaceAll(opt.Left, "%", "")) - left = legendDraw.Box.Width() * value / 100 - } else { - value, _ := strconv.Atoi(opt.Left) - left = value - } - } - x = left - for index, text := range opt.Data { - textBox := r.MeasureText(text) - var renderText func() - if opt.Orient == OrientVertical { - // 垂直 - // 重置x的位置 - x = left - renderText = func() { - x += textPadding - legendDraw.text(text, x, y+legendDotHeight) - x += textBox.Width() - y += (2*legendDotHeight + legendMargin) - } - - } else { - // 水平 - if index != 0 { - x += legendMargin - } - renderText = func() { - x += textPadding - legendDraw.text(text, x, y+legendDotHeight) - x += textBox.Width() - x += textPadding - } - } - if opt.Align == PositionRight { - renderText() - } - seriesColor := theme.GetSeriesColor(index) - fillColor := seriesColor - if !theme.IsDark() { - fillColor = theme.GetBackgroundColor() - } - style := chart.Style{ - StrokeColor: seriesColor, - FillColor: fillColor, - StrokeWidth: 3, - } - if opt.Icon == LegendIconRect { - style.FillColor = seriesColor - style.StrokeWidth = 1 - } - style.GetFillAndStrokeOptions().WriteDrawingOptionsToRenderer(r) - - if opt.Icon == LegendIconRect { - legendDraw.moveTo(x, y-legendDotHeight) - legendDraw.lineTo(x+legendWidth, y-legendDotHeight) - legendDraw.lineTo(x+legendWidth, y+legendDotHeight) - legendDraw.lineTo(x, y+legendDotHeight) - legendDraw.lineTo(x, y-legendDotHeight) - r.FillStroke() - } else { - legendDraw.moveTo(x, y) - legendDraw.lineTo(x+legendWidth, y) - r.Stroke() - legendDraw.circle(float64(legendDotHeight), x+legendWidth>>1, y) - r.FillStroke() - } - x += legendWidth - - if opt.Align != PositionRight { - renderText() - } - } - legendBox := padding.Clone() - // 计算展示区域 - if opt.Orient == OrientVertical { - legendBox.Right = legendBox.Left + left + maxTextWidth + legendWidth + textPadding - legendBox.Bottom = legendBox.Top + y - } else { - legendBox.Right = legendBox.Left + x - legendBox.Bottom = legendBox.Top + 2*legendDotHeight + top + textPadding - } - return legendBox, nil -} diff --git a/legend_test.go b/legend_test.go deleted file mode 100644 index c5d7e50..0000000 --- a/legend_test.go +++ /dev/null @@ -1,185 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -func TestNewLegendOption(t *testing.T) { - assert := assert.New(t) - - opt := NewLegendOption([]string{ - "a", - "b", - }, PositionRight) - assert.Equal(LegendOption{ - Data: []string{ - "a", - "b", - }, - Left: PositionRight, - }, opt) -} - -func TestLegendRender(t *testing.T) { - assert := assert.New(t) - - newDraw := func() *Draw { - d, _ := NewDraw(DrawOption{ - Width: 400, - Height: 300, - }) - return d - } - style := chart.Style{ - FontSize: 10, - FontColor: drawing.ColorBlack, - } - style.Font, _ = chart.GetDefaultFont() - - tests := []struct { - newDraw func() *Draw - newLegend func(*Draw) *legend - box chart.Box - result string - }{ - { - newDraw: newDraw, - newLegend: func(d *Draw) *legend { - return NewLegend(d, LegendOption{ - Top: "10", - Data: []string{ - "Mon", - "Tue", - "Wed", - }, - Style: style, - }) - }, - result: "\\nMonTueWed", - box: chart.Box{ - Right: 214, - Bottom: 25, - }, - }, - { - newDraw: newDraw, - newLegend: func(d *Draw) *legend { - return NewLegend(d, LegendOption{ - Top: "10", - Left: PositionRight, - Align: PositionRight, - Data: []string{ - "Mon", - "Tue", - "Wed", - }, - Style: style, - }) - }, - result: "\\nMonTueWed", - box: chart.Box{ - Right: 400, - Bottom: 25, - }, - }, - { - newDraw: newDraw, - newLegend: func(d *Draw) *legend { - return NewLegend(d, LegendOption{ - Top: "10", - Left: PositionCenter, - Data: []string{ - "Mon", - "Tue", - "Wed", - }, - Style: style, - }) - }, - result: "\\nMonTueWed", - box: chart.Box{ - Right: 307, - Bottom: 25, - }, - }, - { - newDraw: newDraw, - newLegend: func(d *Draw) *legend { - return NewLegend(d, LegendOption{ - Top: "10", - Left: PositionLeft, - Data: []string{ - "Mon", - "Tue", - "Wed", - }, - Style: style, - Orient: OrientVertical, - }) - }, - result: "\\nMonTueWed", - box: chart.Box{ - Right: 61, - Bottom: 80, - }, - }, - { - newDraw: newDraw, - newLegend: func(d *Draw) *legend { - return NewLegend(d, LegendOption{ - Top: "10", - Left: "10%", - Data: []string{ - "Mon", - "Tue", - "Wed", - }, - Style: style, - Orient: OrientVertical, - }) - }, - box: chart.Box{ - Right: 101, - Bottom: 80, - }, - result: "\\nMonTueWed", - }, - } - - for _, tt := range tests { - d := tt.newDraw() - b, err := tt.newLegend(d).Render() - assert.Nil(err) - assert.Equal(tt.box, b) - data, err := d.Bytes() - assert.Nil(err) - assert.NotEmpty(data) - assert.Equal(tt.result, string(data)) - } -} diff --git a/line.go b/line.go index 15ab575..e4b1f18 100644 --- a/line.go +++ b/line.go @@ -22,10 +22,6 @@ package charts -import ( - "github.com/wcharczuk/go-chart/v2" -) - type LineStyle struct { ClassName string StrokeDashArray []float64 @@ -37,8 +33,8 @@ type LineStyle struct { DotFillColor Color } -func (ls *LineStyle) Style() chart.Style { - return chart.Style{ +func (ls *LineStyle) Style() Style { + return Style{ ClassName: ls.ClassName, StrokeDashArray: ls.StrokeDashArray, StrokeColor: ls.StrokeColor, @@ -48,55 +44,3 @@ func (ls *LineStyle) Style() chart.Style { DotColor: ls.DotColor, } } - -func (d *Draw) lineFill(points []Point, style LineStyle) { - s := style.Style() - if !(s.ShouldDrawStroke() && s.ShouldDrawFill()) { - return - } - - newPoints := make([]Point, len(points)) - copy(newPoints, points) - x0 := points[0].X - y0 := points[0].Y - height := d.Box.Height() - newPoints = append(newPoints, Point{ - X: points[len(points)-1].X, - Y: height, - }, Point{ - X: x0, - Y: height, - }, Point{ - X: x0, - Y: y0, - }) - d.fill(newPoints, style.Style()) -} - -func (d *Draw) lineDot(points []Point, style LineStyle) { - s := style.Style() - if !s.ShouldDrawDot() { - return - } - r := d.Render - dotWith := s.GetDotWidth() - - s.GetDotOptions().WriteDrawingOptionsToRenderer(r) - for _, point := range points { - if !style.DotFillColor.IsZero() { - r.SetFillColor(style.DotFillColor) - } - r.SetStrokeColor(s.DotColor) - d.circle(dotWith, point.X, point.Y) - r.FillStroke() - } -} - -func (d *Draw) Line(points []Point, style LineStyle) { - if len(points) == 0 { - return - } - d.lineFill(points, style) - d.lineStroke(points, style) - d.lineDot(points, style) -} diff --git a/line_chart.go b/line_chart.go deleted file mode 100644 index ac9091c..0000000 --- a/line_chart.go +++ /dev/null @@ -1,131 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -type lineChartOption struct { - Theme string - SeriesList SeriesList - Font *truetype.Font -} - -func lineChartRender(opt lineChartOption, result *basicRenderResult) ([]markPointRenderOption, error) { - - theme := NewTheme(opt.Theme) - - d, err := NewDraw(DrawOption{ - Parent: result.d, - }, PaddingOption(chart.Box{ - Top: result.titleBox.Height(), - Left: YAxisWidth, - })) - if err != nil { - return nil, err - } - seriesNames := opt.SeriesList.Names() - - r := d.Render - xRange := result.xRange - markPointRenderOptions := make([]markPointRenderOption, 0) - for i, s := range opt.SeriesList { - // 由于series是for range,为同一个数据,因此需要clone - // 后续需要使用,如mark point - series := s - index := series.index - if index == 0 { - index = i - } - seriesColor := theme.GetSeriesColor(index) - - yRange := result.getYRange(series.YAxisIndex) - points := make([]Point, 0, len(series.Data)) - // mark line - markLineRender(markLineRenderOption{ - Draw: d, - FillColor: seriesColor, - FontColor: theme.GetTextColor(), - StrokeColor: seriesColor, - Font: opt.Font, - Series: &series, - Range: yRange, - }) - - for j, item := range series.Data { - if j >= xRange.divideCount { - continue - } - y := yRange.getRestHeight(item.Value) - x := xRange.getWidth(float64(j)) - points = append(points, Point{ - Y: y, - X: x, - }) - if !series.Label.Show { - continue - } - distance := series.Label.Distance - if distance == 0 { - distance = 5 - } - text := NewValueLabelFormater(seriesNames, series.Label.Formatter)(i, item.Value, -1) - labelStyle := chart.Style{ - FontColor: theme.GetTextColor(), - FontSize: labelFontSize, - Font: opt.Font, - } - if !series.Label.Color.IsZero() { - labelStyle.FontColor = series.Label.Color - } - labelStyle.GetTextOptions().WriteToRenderer(r) - textBox := r.MeasureText(text) - d.text(text, x-textBox.Width()>>1, y-distance) - } - - dotFillColor := drawing.ColorWhite - if theme.IsDark() { - dotFillColor = seriesColor - } - d.Line(points, LineStyle{ - StrokeColor: seriesColor, - StrokeWidth: 2, - DotColor: seriesColor, - DotWidth: defaultDotWidth, - DotFillColor: dotFillColor, - }) - // draw mark point - markPointRenderOptions = append(markPointRenderOptions, markPointRenderOption{ - Draw: d, - FillColor: seriesColor, - Font: opt.Font, - Points: points, - Series: &series, - }) - } - - return markPointRenderOptions, nil -} diff --git a/line_chart_test.go b/line_chart_test.go deleted file mode 100644 index 9f5d9af..0000000 --- a/line_chart_test.go +++ /dev/null @@ -1,97 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -func TestLineChartRender(t *testing.T) { - assert := assert.New(t) - - width := 400 - height := 300 - d, err := NewDraw(DrawOption{ - Width: width, - Height: height, - }) - assert.Nil(err) - - result := basicRenderResult{ - xRange: &Range{ - Min: 0, - Max: 4, - divideCount: 4, - Size: width, - Boundary: true, - }, - yRangeList: []*Range{ - { - divideCount: 6, - Max: 100, - Min: 0, - Size: height, - }, - }, - d: d, - } - f, _ := chart.GetDefaultFont() - _, err = lineChartRender(lineChartOption{ - Font: f, - SeriesList: SeriesList{ - { - Label: SeriesLabel{ - Show: true, - Color: drawing.ColorBlue, - }, - MarkLine: NewMarkLine( - SeriesMarkDataTypeAverage, - ), - Data: []SeriesData{ - { - Value: 20, - }, - { - Value: 60, - }, - { - Value: 90, - }, - }, - }, - NewSeriesFromValues([]float64{ - 40, - 60, - 70, - }), - }, - }, &result) - assert.Nil(err) - data, err := d.Bytes() - assert.Nil(err) - assert.Equal("\\n56.66206090", string(data)) -} diff --git a/line_test.go b/line_test.go deleted file mode 100644 index e10b806..0000000 --- a/line_test.go +++ /dev/null @@ -1,165 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -func TestLineStyle(t *testing.T) { - assert := assert.New(t) - - ls := LineStyle{ - ClassName: "test", - StrokeDashArray: []float64{ - 1.0, - }, - StrokeColor: drawing.ColorBlack, - StrokeWidth: 1, - FillColor: drawing.ColorBlack.WithAlpha(60), - DotWidth: 2, - DotColor: drawing.ColorBlack, - DotFillColor: drawing.ColorWhite, - } - - assert.Equal(chart.Style{ - ClassName: "test", - StrokeDashArray: []float64{ - 1.0, - }, - StrokeColor: drawing.ColorBlack, - StrokeWidth: 1, - FillColor: drawing.ColorBlack.WithAlpha(60), - DotWidth: 2, - DotColor: drawing.ColorBlack, - }, ls.Style()) -} - -func TestDrawLineFill(t *testing.T) { - assert := assert.New(t) - - ls := LineStyle{ - StrokeColor: drawing.ColorBlack, - StrokeWidth: 1, - FillColor: drawing.ColorBlack.WithAlpha(60), - DotWidth: 2, - DotColor: drawing.ColorBlack, - DotFillColor: drawing.ColorWhite, - } - d, err := NewDraw(DrawOption{ - Width: 400, - Height: 300, - }) - assert.Nil(err) - d.lineFill([]Point{ - { - X: 0, - Y: 0, - }, - { - X: 10, - Y: 20, - }, - { - X: 50, - Y: 60, - }, - }, ls) - data, err := d.Bytes() - assert.Nil(err) - assert.Equal("\\n", string(data)) -} - -func TestDrawLineDot(t *testing.T) { - assert := assert.New(t) - - ls := LineStyle{ - StrokeColor: drawing.ColorBlack, - StrokeWidth: 1, - FillColor: drawing.ColorBlack.WithAlpha(60), - DotWidth: 2, - DotColor: drawing.ColorBlack, - DotFillColor: drawing.ColorWhite, - } - d, err := NewDraw(DrawOption{ - Width: 400, - Height: 300, - }) - assert.Nil(err) - d.lineDot([]Point{ - { - X: 0, - Y: 0, - }, - { - X: 10, - Y: 20, - }, - { - X: 50, - Y: 60, - }, - }, ls) - data, err := d.Bytes() - assert.Nil(err) - assert.Equal("\\n", string(data)) -} - -func TestDrawLine(t *testing.T) { - assert := assert.New(t) - - ls := LineStyle{ - StrokeColor: drawing.ColorBlack, - StrokeWidth: 1, - FillColor: drawing.ColorBlack.WithAlpha(60), - DotWidth: 2, - DotColor: drawing.ColorBlack, - DotFillColor: drawing.ColorWhite, - } - d, err := NewDraw(DrawOption{ - Width: 400, - Height: 300, - }) - assert.Nil(err) - d.Line([]Point{ - { - X: 0, - Y: 0, - }, - { - X: 10, - Y: 20, - }, - { - X: 50, - Y: 60, - }, - }, ls) - data, err := d.Bytes() - assert.Nil(err) - assert.Equal("\\n", string(data)) -} diff --git a/mark_line.go b/mark_line.go deleted file mode 100644 index 464fe71..0000000 --- a/mark_line.go +++ /dev/null @@ -1,92 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -func NewMarkLine(markLineTypes ...string) SeriesMarkLine { - data := make([]SeriesMarkData, len(markLineTypes)) - for index, t := range markLineTypes { - data[index] = SeriesMarkData{ - Type: t, - } - } - return SeriesMarkLine{ - Data: data, - } -} - -type markLineRenderOption struct { - Draw *Draw - FillColor drawing.Color - FontColor drawing.Color - StrokeColor drawing.Color - Font *truetype.Font - Series *Series - Range *Range -} - -func markLineRender(opt markLineRenderOption) { - d := opt.Draw - s := opt.Series - if len(s.MarkLine.Data) == 0 { - return - } - r := d.Render - summary := s.Summary() - for _, markLine := range s.MarkLine.Data { - // 由于mark line会修改style,因此每次重新设置 - chart.Style{ - FillColor: opt.FillColor, - FontColor: opt.FontColor, - FontSize: labelFontSize, - StrokeColor: opt.StrokeColor, - StrokeWidth: 1, - Font: opt.Font, - StrokeDashArray: []float64{ - 4, - 2, - }, - }.WriteToRenderer(r) - value := float64(0) - switch markLine.Type { - case SeriesMarkDataTypeMax: - value = summary.MaxValue - case SeriesMarkDataTypeMin: - value = summary.MinValue - default: - value = summary.AverageValue - } - y := opt.Range.getRestHeight(value) - width := d.Box.Width() - text := commafWithDigits(value) - textBox := r.MeasureText(text) - d.makeLine(0, y, width-2) - d.text(text, width, y+textBox.Height()>>1-2) - } - -} diff --git a/mark_line_test.go b/mark_line_test.go deleted file mode 100644 index abb3308..0000000 --- a/mark_line_test.go +++ /dev/null @@ -1,99 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -func TestNewMarkLine(t *testing.T) { - assert := assert.New(t) - - markLine := NewMarkLine( - SeriesMarkDataTypeMax, - SeriesMarkDataTypeMin, - SeriesMarkDataTypeAverage, - ) - - assert.Equal(SeriesMarkLine{ - Data: []SeriesMarkData{ - { - Type: SeriesMarkDataTypeMax, - }, - { - Type: SeriesMarkDataTypeMin, - }, - { - Type: SeriesMarkDataTypeAverage, - }, - }, - }, markLine) -} - -func TestMarkLineRender(t *testing.T) { - assert := assert.New(t) - - d, err := NewDraw(DrawOption{ - Width: 400, - Height: 300, - }, PaddingOption(chart.Box{ - Left: 20, - Right: 20, - })) - assert.Nil(err) - f, _ := chart.GetDefaultFont() - - markLineRender(markLineRenderOption{ - Draw: d, - FillColor: drawing.ColorBlack, - FontColor: drawing.ColorBlack, - StrokeColor: drawing.ColorBlack, - Font: f, - Series: &Series{ - MarkLine: NewMarkLine( - SeriesMarkDataTypeMax, - SeriesMarkDataTypeMin, - SeriesMarkDataTypeAverage, - ), - Data: NewSeriesDataFromValues([]float64{ - 1, - 3, - 5, - 7, - 9, - }), - }, - Range: &Range{ - Min: 0, - Max: 10, - Size: 200, - }, - }) - data, err := d.Bytes() - assert.Nil(err) - assert.Equal("\\n915", string(data)) -} diff --git a/mark_point.go b/mark_point.go deleted file mode 100644 index 5fd34c4..0000000 --- a/mark_point.go +++ /dev/null @@ -1,89 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -func NewMarkPoint(markPointTypes ...string) SeriesMarkPoint { - data := make([]SeriesMarkData, len(markPointTypes)) - for index, t := range markPointTypes { - data[index] = SeriesMarkData{ - Type: t, - } - } - return SeriesMarkPoint{ - Data: data, - } -} - -type markPointRenderOption struct { - Draw *Draw - FillColor drawing.Color - Font *truetype.Font - Series *Series - Points []Point -} - -func markPointRender(opt markPointRenderOption) { - d := opt.Draw - s := opt.Series - if len(s.MarkPoint.Data) == 0 { - return - } - points := opt.Points - summary := s.Summary() - symbolSize := s.MarkPoint.SymbolSize - if symbolSize == 0 { - symbolSize = 30 - } - r := d.Render - // 设置填充样式 - chart.Style{ - FillColor: opt.FillColor, - }.WriteToRenderer(r) - // 设置文本样式 - chart.Style{ - FontColor: NewTheme(ThemeDark).GetTextColor(), - FontSize: labelFontSize, - StrokeWidth: 1, - Font: opt.Font, - }.WriteTextOptionsToRenderer(r) - for _, markPointData := range s.MarkPoint.Data { - p := points[summary.MinIndex] - value := summary.MinValue - switch markPointData.Type { - case SeriesMarkDataTypeMax: - p = points[summary.MaxIndex] - value = summary.MaxValue - } - - d.pin(p.X, p.Y-symbolSize>>1, symbolSize) - text := commafWithDigits(value) - textBox := r.MeasureText(text) - d.text(text, p.X-textBox.Width()>>1, p.Y-symbolSize>>1-2) - } -} diff --git a/mark_point_test.go b/mark_point_test.go deleted file mode 100644 index 2cd8fdd..0000000 --- a/mark_point_test.go +++ /dev/null @@ -1,103 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -func TestNewMarkPoint(t *testing.T) { - assert := assert.New(t) - - markPoint := NewMarkPoint( - SeriesMarkDataTypeMax, - SeriesMarkDataTypeMin, - SeriesMarkDataTypeAverage, - ) - - assert.Equal(SeriesMarkPoint{ - Data: []SeriesMarkData{ - { - Type: SeriesMarkDataTypeMax, - }, - { - Type: SeriesMarkDataTypeMin, - }, - { - Type: SeriesMarkDataTypeAverage, - }, - }, - }, markPoint) -} - -func TestMarkPointRender(t *testing.T) { - assert := assert.New(t) - - d, err := NewDraw(DrawOption{ - Width: 400, - Height: 300, - }, PaddingOption(chart.Box{ - Left: 20, - Right: 20, - })) - assert.Nil(err) - f, _ := chart.GetDefaultFont() - - markPointRender(markPointRenderOption{ - Draw: d, - FillColor: drawing.ColorBlack, - Font: f, - Series: &Series{ - MarkPoint: NewMarkPoint( - SeriesMarkDataTypeMax, - SeriesMarkDataTypeMin, - ), - Data: NewSeriesDataFromValues([]float64{ - 1, - 3, - 5, - }), - }, - Points: []Point{ - { - X: 1, - Y: 50, - }, - { - X: 100, - Y: 100, - }, - { - X: 200, - Y: 200, - }, - }, - }) - data, err := d.Bytes() - assert.Nil(err) - assert.Equal("\\n51", string(data)) -} diff --git a/painter.go b/painter.go index 639371e..d762e86 100644 --- a/painter.go +++ b/painter.go @@ -135,6 +135,8 @@ func NewPainter(opts PainterOptions, opt ...PainterOption) (*Painter, error) { if err != nil { return nil, err } + r.SetFont(font) + p := &Painter{ render: r, box: Box{ @@ -167,6 +169,9 @@ func (p *Painter) Child(opt ...PainterOption) *Painter { } func (p *Painter) SetStyle(style Style) { + if style.Font == nil { + style.Font = p.font + } p.previousStyle = p.style p.style = style style.WriteToRenderer(p.render) @@ -179,6 +184,9 @@ func (p *Painter) SetDrawingStyle(style Style) { } func (p *Painter) SetTextStyle(style Style) { + if style.Font == nil { + style.Font = p.font + } p.previousStyle = p.style p.style = style style.WriteTextOptionsToRenderer(p.render) diff --git a/painter_test.go b/painter_test.go index 425dbbe..1cc08be 100644 --- a/painter_test.go +++ b/painter_test.go @@ -94,7 +94,7 @@ func TestPainter(t *testing.T) { fn: func(p *Painter) { p.Text("hello world!", 3, 6) }, - result: "\\nhello world!", + result: "\\nhello world!", }, // line stroke { diff --git a/pie_chart.go b/pie_chart.go deleted file mode 100644 index 099a91c..0000000 --- a/pie_chart.go +++ /dev/null @@ -1,159 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "errors" - "math" - - "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" -) - -func getPieStyle(theme *Theme, index int) chart.Style { - seriesColor := theme.GetSeriesColor(index) - return chart.Style{ - StrokeColor: seriesColor, - StrokeWidth: 1, - FillColor: seriesColor, - } -} - -type pieChartOption struct { - Theme string - Font *truetype.Font - SeriesList SeriesList -} - -func pieChartRender(opt pieChartOption, result *basicRenderResult) error { - d, err := NewDraw(DrawOption{ - Parent: result.d, - }, PaddingOption(chart.Box{ - Top: result.titleBox.Height(), - })) - if err != nil { - return err - } - - values := make([]float64, len(opt.SeriesList)) - total := float64(0) - radiusValue := "" - for index, series := range opt.SeriesList { - if len(series.Radius) != 0 { - radiusValue = series.Radius - } - value := float64(0) - for _, item := range series.Data { - value += item.Value - } - values[index] = value - total += value - } - if total <= 0 { - return errors.New("The sum value of pie chart should gt 0") - } - r := d.Render - theme := NewTheme(opt.Theme) - - box := d.Box - cx := box.Width() >> 1 - cy := box.Height() >> 1 - - diameter := chart.MinInt(box.Width(), box.Height()) - radius := getRadius(float64(diameter), radiusValue) - - labelLineWidth := 15 - if radius < 50 { - labelLineWidth = 10 - } - labelRadius := radius + float64(labelLineWidth) - - seriesNames := opt.SeriesList.Names() - - if len(values) == 1 { - getPieStyle(theme, 0).WriteToRenderer(r) - d.moveTo(cx, cy) - d.circle(radius, cx, cy) - } else { - currentValue := float64(0) - for index, v := range values { - - pieStyle := getPieStyle(theme, index) - pieStyle.WriteToRenderer(r) - d.moveTo(cx, cy) - start := chart.PercentToRadians(currentValue/total) - math.Pi/2 - currentValue += v - percent := (v / total) - delta := chart.PercentToRadians(percent) - d.arcTo(cx, cy, radius, radius, start, delta) - d.lineTo(cx, cy) - r.Close() - r.FillStroke() - - series := opt.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)) - d.moveTo(startx, starty) - d.lineTo(endx, endy) - offset := labelLineWidth - if endx < cx { - offset *= -1 - } - d.moveTo(endx, endy) - endx += offset - d.lineTo(endx, endy) - r.Stroke() - textStyle := chart.Style{ - FontColor: theme.GetTextColor(), - FontSize: labelFontSize, - Font: opt.Font, - } - if !series.Label.Color.IsZero() { - textStyle.FontColor = series.Label.Color - } - textStyle.GetTextOptions().WriteToRenderer(r) - text := NewPieLabelFormatter(seriesNames, series.Label.Formatter)(index, v, percent) - textBox := r.MeasureText(text) - textMargin := 3 - x := endx + textMargin - y := endy + textBox.Height()>>1 - 1 - if offset < 0 { - textWidth := textBox.Width() - x = endx - textWidth - textMargin - } - d.text(text, x, y) - } - } - return nil -} diff --git a/pie_chart_test.go b/pie_chart_test.go deleted file mode 100644 index 84072be..0000000 --- a/pie_chart_test.go +++ /dev/null @@ -1,69 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -func TestPieChartRender(t *testing.T) { - assert := assert.New(t) - - d, err := NewDraw(DrawOption{ - Width: 250, - Height: 150, - }) - assert.Nil(err) - - f, _ := chart.GetDefaultFont() - - err = pieChartRender(pieChartOption{ - Font: f, - SeriesList: NewPieSeriesList([]float64{ - 5, - 10, - 0, - }, PieSeriesOption{ - Names: []string{ - "a", - "b", - "c", - }, - Label: SeriesLabel{ - Show: true, - Color: drawing.ColorRed, - }, - Radius: "20%", - }), - }, &basicRenderResult{ - d: d, - }) - assert.Nil(err) - data, err := d.Bytes() - assert.Nil(err) - assert.Equal("\\na: 33.33%b: 66.66%c: 0%", string(data)) -} diff --git a/radar_chart.go b/radar_chart.go deleted file mode 100644 index 364213d..0000000 --- a/radar_chart.go +++ /dev/null @@ -1,193 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "errors" - - "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -type RadarIndicator struct { - // Indicator's name - Name string - // The maximum value of indicator - Max float64 - // The minimum value of indicator - Min float64 -} - -type radarChartOption struct { - Theme string - Font *truetype.Font - SeriesList SeriesList - Indicators []RadarIndicator -} - -func radarChartRender(opt radarChartOption, result *basicRenderResult) error { - sides := len(opt.Indicators) - if sides < 3 { - return errors.New("The count of indicator should be >= 3") - } - maxValues := make([]float64, len(opt.Indicators)) - for _, series := range opt.SeriesList { - for index, item := range series.Data { - if index < len(maxValues) && item.Value > maxValues[index] { - maxValues[index] = item.Value - } - } - } - for index, indicator := range opt.Indicators { - if indicator.Max <= 0 { - opt.Indicators[index].Max = maxValues[index] - } - } - d, err := NewDraw(DrawOption{ - Parent: result.d, - }, PaddingOption(chart.Box{ - Top: result.titleBox.Height(), - })) - if err != nil { - return err - } - radiusValue := "" - for _, series := range opt.SeriesList { - if len(series.Radius) != 0 { - radiusValue = series.Radius - } - } - - box := d.Box - cx := box.Width() >> 1 - cy := box.Height() >> 1 - diameter := chart.MinInt(box.Width(), box.Height()) - radius := getRadius(float64(diameter), radiusValue) - - theme := NewTheme(opt.Theme) - - divideCount := 5 - divideRadius := float64(int(radius / float64(divideCount))) - radius = divideRadius * float64(divideCount) - - style := chart.Style{ - StrokeColor: theme.GetAxisSplitLineColor(), - StrokeWidth: 1, - } - r := d.Render - style.WriteToRenderer(r) - center := Point{ - X: cx, - Y: cy, - } - for i := 0; i < divideCount; i++ { - d.polygon(center, divideRadius*float64(i+1), sides) - } - points := getPolygonPoints(center, radius, sides) - for _, p := range points { - d.moveTo(center.X, center.Y) - d.lineTo(p.X, p.Y) - d.Render.Stroke() - } - // 文本 - textStyle := chart.Style{ - FontColor: theme.GetTextColor(), - FontSize: labelFontSize, - Font: opt.Font, - } - textStyle.GetTextOptions().WriteToRenderer(r) - offset := 5 - // 文本生成 - for index, p := range points { - name := opt.Indicators[index].Name - b := r.MeasureText(name) - isXCenter := p.X == center.X - isYCenter := p.Y == center.Y - isRight := p.X > center.X - isLeft := p.X < center.X - isTop := p.Y < center.Y - isBottom := p.Y > center.Y - x := p.X - y := p.Y - if isXCenter { - x -= b.Width() >> 1 - if isTop { - y -= b.Height() - } else { - y += b.Height() - } - } - if isYCenter { - y += b.Height() >> 1 - } - if isTop { - y += offset - } - if isBottom { - y += offset - } - if isRight { - x += offset - } - if isLeft { - x -= (b.Width() + offset) - } - d.text(name, x, y) - } - - // 雷达图 - angles := getPolygonPointAngles(sides) - maxCount := len(opt.Indicators) - for _, series := range opt.SeriesList { - linePoints := make([]Point, 0, maxCount) - for j, item := range series.Data { - if j >= maxCount { - continue - } - indicator := opt.Indicators[j] - percent := (item.Value - indicator.Min) / (indicator.Max - indicator.Min) - r := percent * radius - p := getPolygonPoint(center, r, angles[j]) - linePoints = append(linePoints, p) - } - color := theme.GetSeriesColor(series.index) - dotFillColor := drawing.ColorWhite - if theme.IsDark() { - dotFillColor = color - } - linePoints = append(linePoints, linePoints[0]) - s := LineStyle{ - StrokeColor: color, - StrokeWidth: defaultStrokeWidth, - DotWidth: defaultDotWidth, - DotColor: color, - DotFillColor: dotFillColor, - FillColor: color.WithAlpha(20), - } - d.lineStroke(linePoints, s) - d.fill(linePoints, s.Style()) - d.lineDot(linePoints[0:len(linePoints)-1], s) - } - return nil -} diff --git a/radar_chart_test.go b/radar_chart_test.go deleted file mode 100644 index c5d2aa9..0000000 --- a/radar_chart_test.go +++ /dev/null @@ -1,102 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2" -) - -func TestRadarChartRender(t *testing.T) { - assert := assert.New(t) - - d, err := NewDraw(DrawOption{ - Width: 250, - Height: 150, - }) - assert.Nil(err) - - f, _ := chart.GetDefaultFont() - err = radarChartRender(radarChartOption{ - Font: f, - Indicators: []RadarIndicator{ - { - Name: "Sales", - Max: 6500, - }, - { - Name: "Administration", - Max: 16000, - }, - { - Name: "Information Technology", - Max: 30000, - }, - { - Name: "Customer Support", - Max: 38000, - }, - { - Name: "Development", - Max: 52000, - }, - { - Name: "Marketing", - Max: 25000, - }, - }, - SeriesList: SeriesList{ - { - Type: ChartTypeRadar, - Data: NewSeriesDataFromValues([]float64{ - 4200, - 3000, - 20000, - 35000, - 50000, - 18000, - }), - }, - { - Type: ChartTypeRadar, - index: 1, - Data: NewSeriesDataFromValues([]float64{ - 5000, - 14000, - 28000, - 26000, - 42000, - 21000, - }), - }, - }, - }, &basicRenderResult{ - d: d, - }) - assert.Nil(err) - data, err := d.Bytes() - assert.Nil(err) - assert.Equal("\\nSalesAdministrationInformation TechnologyCustomer SupportDevelopmentMarketing", string(data)) -} diff --git a/range.go b/range.go deleted file mode 100644 index 255a51b..0000000 --- a/range.go +++ /dev/null @@ -1,109 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "math" -) - -type Range struct { - divideCount int - Min float64 - Max float64 - Size int - Boundary bool -} - -func NewRange(min, max float64, divideCount int) Range { - r := math.Abs(max - min) - - // 最小单位计算 - unit := 2 - if r > 10 { - unit = 4 - } - if r > 30 { - unit = 5 - } - if r > 100 { - unit = 10 - } - if r > 200 { - unit = 20 - } - unit = int((r/float64(divideCount))/float64(unit))*unit + unit - - if min != 0 { - isLessThanZero := min < 0 - min = float64(int(min/float64(unit)) * unit) - // 如果是小于0,int的时候向上取整了,因此调整 - if min < 0 || - (isLessThanZero && min == 0) { - min -= float64(unit) - } - } - max = min + float64(unit*divideCount) - return Range{ - Min: min, - Max: max, - divideCount: divideCount, - } -} - -func (r Range) Values() []string { - offset := (r.Max - r.Min) / float64(r.divideCount) - values := make([]string, 0) - for i := 0; i <= r.divideCount; i++ { - v := r.Min + float64(i)*offset - value := commafWithDigits(v) - values = append(values, value) - } - return values -} - -func (r *Range) getHeight(value float64) int { - v := (value - r.Min) / (r.Max - r.Min) - return int(v * float64(r.Size)) -} - -func (r *Range) getRestHeight(value float64) int { - return r.Size - r.getHeight(value) -} - -func (r *Range) GetRange(index int) (float64, float64) { - unit := float64(r.Size) / float64(r.divideCount) - return unit * float64(index), unit * float64(index+1) -} -func (r *Range) AutoDivide() []int { - return autoDivide(r.Size, r.divideCount) -} - -func (r *Range) getWidth(value float64) int { - v := value / (r.Max - r.Min) - // 移至居中 - if r.Boundary && - r.divideCount != 0 { - v += 1 / float64(r.divideCount*2) - } - return int(v * float64(r.Size)) -} diff --git a/range_test.go b/range_test.go deleted file mode 100644 index d1aea8f..0000000 --- a/range_test.go +++ /dev/null @@ -1,94 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestRange(t *testing.T) { - assert := assert.New(t) - - r := NewRange(0, 8, 6) - assert.Equal(0.0, r.Min) - assert.Equal(12.0, r.Max) - - r = NewRange(0, 12, 6) - assert.Equal(0.0, r.Min) - assert.Equal(24.0, r.Max) - - r = NewRange(-13, 18, 6) - assert.Equal(-20.0, r.Min) - assert.Equal(40.0, r.Max) - - r = NewRange(0, 150, 6) - assert.Equal(0.0, r.Min) - assert.Equal(180.0, r.Max) - - r = NewRange(0, 400, 6) - assert.Equal(0.0, r.Min) - assert.Equal(480.0, r.Max) -} - -func TestRangeHeightWidth(t *testing.T) { - assert := assert.New(t) - r := NewRange(0, 8, 6) - r.Size = 100 - - assert.Equal(33, r.getHeight(4)) - assert.Equal(67, r.getRestHeight(4)) - - assert.Equal(33, r.getWidth(4)) - r.Boundary = true - assert.Equal(41, r.getWidth(4)) -} - -func TestRangeGetRange(t *testing.T) { - assert := assert.New(t) - r := NewRange(0, 8, 6) - r.Size = 120 - - f1, f2 := r.GetRange(0) - assert.Equal(0.0, f1) - assert.Equal(20.0, f2) - - f1, f2 = r.GetRange(2) - assert.Equal(40.0, f1) - assert.Equal(60.0, f2) -} - -func TestRangeAutoDivide(t *testing.T) { - assert := assert.New(t) - - r := Range{ - Size: 120, - divideCount: 6, - } - - assert.Equal([]int{0, 20, 40, 60, 80, 100, 120}, r.AutoDivide()) - - r.Size = 130 - assert.Equal([]int{0, 22, 44, 66, 88, 109, 130}, r.AutoDivide()) -} diff --git a/series.go b/series.go deleted file mode 100644 index 14227d1..0000000 --- a/series.go +++ /dev/null @@ -1,233 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "math" - "strings" - - "github.com/dustin/go-humanize" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -type SeriesData struct { - // The value of series data - Value float64 - // The style of series data - Style chart.Style -} - -func NewSeriesFromValues(values []float64, chartType ...string) Series { - s := Series{ - Data: NewSeriesDataFromValues(values), - } - if len(chartType) != 0 { - s.Type = chartType[0] - } - return s -} - -func NewSeriesDataFromValues(values []float64) []SeriesData { - data := make([]SeriesData, len(values)) - for index, value := range values { - data[index] = SeriesData{ - Value: value, - } - } - return data -} - -type SeriesLabel struct { - // Data label formatter, which supports string template. - // {b}: the name of a data item. - // {c}: the value of a data item. - // {d}: the percent of a data item(pie chart). - Formatter string - // The color for label - Color drawing.Color - // Show flag for label - Show bool - // Distance to the host graphic element. - Distance int -} - -const ( - SeriesMarkDataTypeMax = "max" - SeriesMarkDataTypeMin = "min" - SeriesMarkDataTypeAverage = "average" -) - -type SeriesMarkData struct { - // The mark data type, it can be "max", "min", "average". - // The "average" is only for mark line - Type string -} -type SeriesMarkPoint struct { - // The width of symbol, default value is 30 - SymbolSize int - // The mark data of series mark point - Data []SeriesMarkData -} -type SeriesMarkLine struct { - // The mark data of series mark line - Data []SeriesMarkData -} -type Series struct { - index int - // The type of series, it can be "line", "bar" or "pie". - // Default value is "line" - Type string - // The data list of series - Data []SeriesData - // The Y axis index, it should be 0 or 1. - // Default value is 1 - YAxisIndex int - // The style for series - Style chart.Style - // The label for series - Label SeriesLabel - // The name of series - Name string - // Radius for Pie chart, e.g.: 40%, default is "40%" - Radius string - // Mark point for series - MarkPoint SeriesMarkPoint - // Make line for series - MarkLine SeriesMarkLine - // Max value of series - Min *float64 - // Min value of series - Max *float64 -} -type SeriesList []Series - -type PieSeriesOption struct { - Radius string - Label SeriesLabel - Names []string -} - -func NewPieSeriesList(values []float64, opts ...PieSeriesOption) SeriesList { - result := make([]Series, len(values)) - var opt PieSeriesOption - if len(opts) != 0 { - opt = opts[0] - } - for index, v := range values { - name := "" - if index < len(opt.Names) { - name = opt.Names[index] - } - s := Series{ - Type: ChartTypePie, - Data: []SeriesData{ - { - Value: v, - }, - }, - Radius: opt.Radius, - Label: opt.Label, - Name: name, - } - result[index] = s - } - return result -} - -type seriesSummary struct { - MaxIndex int - MaxValue float64 - MinIndex int - MinValue float64 - AverageValue float64 -} - -func (s *Series) Summary() seriesSummary { - minIndex := -1 - maxIndex := -1 - minValue := math.MaxFloat64 - maxValue := -math.MaxFloat64 - sum := float64(0) - for j, item := range s.Data { - if item.Value < minValue { - minIndex = j - minValue = item.Value - } - if item.Value > maxValue { - maxIndex = j - maxValue = item.Value - } - sum += item.Value - } - return seriesSummary{ - MaxIndex: maxIndex, - MaxValue: maxValue, - MinIndex: minIndex, - MinValue: minValue, - AverageValue: sum / float64(len(s.Data)), - } -} - -func (sl SeriesList) Names() []string { - names := make([]string, len(sl)) - for index, s := range sl { - names[index] = s.Name - } - return names -} - -type LabelFormatter func(index int, value float64, percent float64) string - -func NewPieLabelFormatter(seriesNames []string, layout string) LabelFormatter { - if len(layout) == 0 { - layout = "{b}: {d}" - } - return NewLabelFormatter(seriesNames, layout) -} - -func NewValueLabelFormater(seriesNames []string, layout string) LabelFormatter { - if len(layout) == 0 { - layout = "{c}" - } - return NewLabelFormatter(seriesNames, layout) -} - -func NewLabelFormatter(seriesNames []string, layout string) LabelFormatter { - return func(index int, value, percent float64) string { - // 如果无percent的则设置为<0 - percentText := "" - if percent >= 0 { - percentText = humanize.FtoaWithDigits(percent*100, 2) + "%" - } - valueText := humanize.FtoaWithDigits(value, 2) - name := "" - if len(seriesNames) > index { - name = seriesNames[index] - } - text := strings.ReplaceAll(layout, "{c}", valueText) - text = strings.ReplaceAll(text, "{d}", percentText) - text = strings.ReplaceAll(text, "{b}", name) - return text - } -} diff --git a/series_test.go b/series_test.go deleted file mode 100644 index 1460180..0000000 --- a/series_test.go +++ /dev/null @@ -1,166 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestNewSeriesFromValues(t *testing.T) { - assert := assert.New(t) - - assert.Equal(Series{ - Data: []SeriesData{ - { - Value: 1, - }, - { - Value: 2, - }, - }, - Type: ChartTypeBar, - }, NewSeriesFromValues([]float64{ - 1, - 2, - }, ChartTypeBar)) -} - -func TestNewSeriesDataFromValues(t *testing.T) { - assert := assert.New(t) - - assert.Equal([]SeriesData{ - { - Value: 1, - }, - { - Value: 2, - }, - }, NewSeriesDataFromValues([]float64{ - 1, - 2, - })) -} - -func TestNewPieSeriesList(t *testing.T) { - assert := assert.New(t) - - assert.Equal(SeriesList{ - { - Type: ChartTypePie, - Name: "a", - Label: SeriesLabel{ - Show: true, - }, - Radius: "30%", - Data: []SeriesData{ - { - Value: 1, - }, - }, - }, - { - Type: ChartTypePie, - Name: "b", - Label: SeriesLabel{ - Show: true, - }, - Radius: "30%", - Data: []SeriesData{ - { - Value: 2, - }, - }, - }, - }, NewPieSeriesList([]float64{ - 1, - 2, - }, PieSeriesOption{ - Radius: "30%", - Label: SeriesLabel{ - Show: true, - }, - Names: []string{ - "a", - "b", - }, - })) -} - -func TestSeriesSummary(t *testing.T) { - assert := assert.New(t) - - s := Series{ - Data: NewSeriesDataFromValues([]float64{ - 1, - 3, - 5, - 7, - 9, - }), - } - assert.Equal(seriesSummary{ - MaxIndex: 4, - MaxValue: 9, - MinIndex: 0, - MinValue: 1, - AverageValue: 5, - }, s.Summary()) -} - -func TestGetSeriesNames(t *testing.T) { - assert := assert.New(t) - - sl := SeriesList{ - { - Name: "a", - }, - { - Name: "b", - }, - } - assert.Equal([]string{ - "a", - "b", - }, sl.Names()) -} - -func TestNewPieLabelFormatter(t *testing.T) { - assert := assert.New(t) - - fn := NewPieLabelFormatter([]string{ - "a", - "b", - }, "") - assert.Equal("a: 35%", fn(0, 1.2, 0.35)) -} - -func TestNewValueLabelFormater(t *testing.T) { - assert := assert.New(t) - fn := NewValueLabelFormater([]string{ - "a", - "b", - }, "") - assert.Equal("1.2", fn(0, 1.2, 0.35)) -} diff --git a/table.go b/table.go deleted file mode 100644 index 9cfc6b1..0000000 --- a/table.go +++ /dev/null @@ -1,145 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "errors" - - "github.com/wcharczuk/go-chart/v2" -) - -type TableOption struct { - // draw - Draw *Draw - // The width of table - Width int - // The header of table - Header []string - // The style of table - Style chart.Style - ColumnWidths []float64 - // 是否仅测量高度 - measurement bool -} - -var ErrTableColumnWidthInvalid = errors.New("table column width is invalid") - -func tableDivideWidth(width, size int, columnWidths []float64) ([]int, error) { - widths := make([]int, size) - - autoFillCount := size - restWidth := width - if len(columnWidths) != 0 { - for index, v := range columnWidths { - if v <= 0 { - continue - } - autoFillCount-- - // 小于1的表示占比 - if v < 1 { - widths[index] = int(v * float64(width)) - } else { - widths[index] = int(v) - } - restWidth -= widths[index] - } - } - if restWidth < 0 { - return nil, ErrTableColumnWidthInvalid - } - // 填充其它未指定的宽度 - if autoFillCount > 0 { - autoWidth := restWidth / autoFillCount - for index, v := range widths { - if v == 0 { - widths[index] = autoWidth - } - } - } - widthSum := 0 - for _, v := range widths { - widthSum += v - } - if widthSum > width { - return nil, ErrTableColumnWidthInvalid - } - return widths, nil -} - -func TableMeasure(opt TableOption) (chart.Box, error) { - d, err := NewDraw(DrawOption{ - Width: opt.Width, - Height: 600, - }) - if err != nil { - return chart.BoxZero, err - } - opt.Draw = d - opt.measurement = true - return tableRender(opt) -} - -func tableRender(opt TableOption) (chart.Box, error) { - if opt.Draw == nil { - return chart.BoxZero, errors.New("draw can not be nil") - } - if len(opt.Header) == 0 { - return chart.BoxZero, errors.New("header can not be nil") - } - width := opt.Width - if width == 0 { - width = opt.Draw.Box.Width() - } - - columnWidths, err := tableDivideWidth(width, len(opt.Header), opt.ColumnWidths) - if err != nil { - return chart.BoxZero, err - } - - d := opt.Draw - style := opt.Style - y := 0 - x := 0 - - headerMaxHeight := 0 - for index, text := range opt.Header { - var box chart.Box - w := columnWidths[index] - y0 := y + int(opt.Style.FontSize) - if opt.measurement { - box = d.measureTextFit(text, x, y0, w, style) - } else { - box = d.textFit(text, x, y0, w, style) - } - if box.Height() > headerMaxHeight { - headerMaxHeight = box.Height() - } - x += w - } - y += headerMaxHeight - - return chart.Box{ - Right: width, - Bottom: y, - }, nil -} diff --git a/theme.go b/theme.go index e3f9773..88c73df 100644 --- a/theme.go +++ b/theme.go @@ -22,10 +22,6 @@ package charts -import ( - "github.com/wcharczuk/go-chart/v2/drawing" -) - const ThemeDark = "dark" const ThemeLight = "light" const ThemeGrafana = "grafana" @@ -37,198 +33,9 @@ type Theme struct { type themeColorPalette struct { isDarkMode bool - axisStrokeColor drawing.Color - axisSplitLineColor drawing.Color - backgroundColor drawing.Color - textColor drawing.Color - seriesColors []drawing.Color -} - -var palettes = map[string]*themeColorPalette{} - -func init() { - echartSeriesColors := []drawing.Color{ - parseColor("#5470c6"), - parseColor("#91cc75"), - parseColor("#fac858"), - parseColor("#ee6666"), - parseColor("#73c0de"), - parseColor("#3ba272"), - parseColor("#fc8452"), - parseColor("#9a60b4"), - parseColor("#ea7ccc"), - } - grafanaSeriesColors := []drawing.Color{ - parseColor("#7EB26D"), - parseColor("#EAB839"), - parseColor("#6ED0E0"), - parseColor("#EF843C"), - parseColor("#E24D42"), - parseColor("#1F78C1"), - parseColor("#705DA0"), - parseColor("#508642"), - } - antSeriesColors := []drawing.Color{ - parseColor("#5b8ff9"), - parseColor("#5ad8a6"), - parseColor("#5d7092"), - parseColor("#f6bd16"), - parseColor("#6f5ef9"), - parseColor("#6dc8ec"), - parseColor("#945fb9"), - parseColor("#ff9845"), - } - AddTheme( - ThemeDark, - true, - drawing.Color{ - R: 185, - G: 184, - B: 206, - A: 255, - }, - drawing.Color{ - R: 72, - G: 71, - B: 83, - A: 255, - }, - drawing.Color{ - R: 16, - G: 12, - B: 42, - A: 255, - }, - drawing.Color{ - R: 238, - G: 238, - B: 238, - A: 255, - }, - echartSeriesColors, - ) - - AddTheme( - ThemeLight, - false, - drawing.Color{ - R: 110, - G: 112, - B: 121, - A: 255, - }, - drawing.Color{ - R: 224, - G: 230, - B: 242, - A: 255, - }, - drawing.ColorWhite, - drawing.Color{ - R: 70, - G: 70, - B: 70, - A: 255, - }, - echartSeriesColors, - ) - AddTheme( - ThemeAnt, - false, - drawing.Color{ - R: 110, - G: 112, - B: 121, - A: 255, - }, - drawing.Color{ - R: 224, - G: 230, - B: 242, - A: 255, - }, - drawing.ColorWhite, - drawing.Color{ - R: 70, - G: 70, - B: 70, - A: 255, - }, - antSeriesColors, - ) - AddTheme( - ThemeGrafana, - true, - drawing.Color{ - R: 185, - G: 184, - B: 206, - A: 255, - }, - drawing.Color{ - R: 68, - G: 67, - B: 67, - A: 255, - }, - drawing.Color{ - R: 31, - G: 29, - B: 29, - A: 255, - }, - drawing.Color{ - R: 216, - G: 217, - B: 218, - A: 255, - }, - grafanaSeriesColors, - ) -} - -func AddTheme(name string, isDarkMode bool, axisStrokeColor, axisSplitLineColor, backgroundColor, textColor drawing.Color, seriesColors []drawing.Color) { - palettes[name] = &themeColorPalette{ - isDarkMode: isDarkMode, - axisStrokeColor: axisStrokeColor, - axisSplitLineColor: axisSplitLineColor, - backgroundColor: backgroundColor, - textColor: textColor, - seriesColors: seriesColors, - } -} - -func NewTheme(name string) *Theme { - p, ok := palettes[name] - if !ok { - p = palettes[ThemeLight] - } - return &Theme{ - palette: p, - } -} - -func (t *Theme) IsDark() bool { - return t.palette.isDarkMode -} - -func (t *Theme) GetAxisStrokeColor() drawing.Color { - return t.palette.axisStrokeColor -} - -func (t *Theme) GetAxisSplitLineColor() drawing.Color { - return t.palette.axisSplitLineColor -} - -func (t *Theme) GetSeriesColor(index int) drawing.Color { - colors := t.palette.seriesColors - return colors[index%len(colors)] -} - -func (t *Theme) GetBackgroundColor() drawing.Color { - return t.palette.backgroundColor -} - -func (t *Theme) GetTextColor() drawing.Color { - return t.palette.textColor + axisStrokeColor Color + axisSplitLineColor Color + backgroundColor Color + textColor Color + seriesColors []Color } diff --git a/theme_test.go b/theme_test.go deleted file mode 100644 index bf22afd..0000000 --- a/theme_test.go +++ /dev/null @@ -1,87 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -func TestTheme(t *testing.T) { - assert := assert.New(t) - - darkTheme := NewTheme(ThemeDark) - lightTheme := NewTheme(ThemeLight) - - assert.True(darkTheme.IsDark()) - assert.False(lightTheme.IsDark()) - - assert.Equal(drawing.Color{ - R: 185, - G: 184, - B: 206, - A: 255, - }, darkTheme.GetAxisStrokeColor()) - assert.Equal(drawing.Color{ - R: 110, - G: 112, - B: 121, - A: 255, - }, lightTheme.GetAxisStrokeColor()) - - assert.Equal(drawing.Color{ - R: 72, - G: 71, - B: 83, - A: 255, - }, darkTheme.GetAxisSplitLineColor()) - assert.Equal(drawing.Color{ - R: 224, - G: 230, - B: 242, - A: 255, - }, lightTheme.GetAxisSplitLineColor()) - - assert.Equal(drawing.Color{ - R: 16, - G: 12, - B: 42, - A: 255, - }, darkTheme.GetBackgroundColor()) - assert.Equal(drawing.ColorWhite, lightTheme.GetBackgroundColor()) - - assert.Equal(drawing.Color{ - R: 238, - G: 238, - B: 238, - A: 255, - }, darkTheme.GetTextColor()) - assert.Equal(drawing.Color{ - R: 70, - G: 70, - B: 70, - A: 255, - }, lightTheme.GetTextColor()) -} diff --git a/title.go b/title.go deleted file mode 100644 index 07a2eef..0000000 --- a/title.go +++ /dev/null @@ -1,155 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "strconv" - "strings" - - "github.com/wcharczuk/go-chart/v2" -) - -type TitleOption struct { - // Title text, support \n for new line - Text string - // Subtitle text, support \n for new line - Subtext string - // Title style - Style chart.Style - // Subtitle style - SubtextStyle chart.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. - Left string - // Distance between title component and the top side of the container. - // It can be pixel value: 20. - Top string -} -type titleMeasureOption struct { - width int - height int - text string - style chart.Style -} - -func splitTitleText(text string) []string { - arr := strings.Split(text, "\n") - result := make([]string, 0) - for _, v := range arr { - v = strings.TrimSpace(v) - if v == "" { - continue - } - result = append(result, v) - } - return result -} - -func drawTitle(p *Draw, opt *TitleOption) (chart.Box, error) { - if len(opt.Text) == 0 { - return chart.BoxZero, nil - } - - padding := opt.Style.Padding - d, err := NewDraw(DrawOption{ - Parent: p, - }, PaddingOption(padding)) - if err != nil { - return chart.BoxZero, err - } - - r := d.Render - - measureOptions := make([]titleMeasureOption, 0) - - // 主标题 - for _, v := range splitTitleText(opt.Text) { - measureOptions = append(measureOptions, titleMeasureOption{ - text: v, - style: opt.Style.GetTextOptions(), - }) - } - // 副标题 - for _, v := range splitTitleText(opt.Subtext) { - measureOptions = append(measureOptions, titleMeasureOption{ - text: v, - style: opt.SubtextStyle.GetTextOptions(), - }) - } - - textMaxWidth := 0 - textMaxHeight := 0 - width := 0 - for index, item := range measureOptions { - item.style.WriteTextOptionsToRenderer(r) - textBox := r.MeasureText(item.text) - - w := textBox.Width() - h := textBox.Height() - if w > textMaxWidth { - textMaxWidth = w - } - if h > textMaxHeight { - textMaxHeight = h - } - measureOptions[index].height = h - measureOptions[index].width = w - } - width = textMaxWidth - titleX := 0 - b := d.Box - switch opt.Left { - case PositionRight: - titleX = b.Width() - textMaxWidth - case PositionCenter: - titleX = b.Width()>>1 - (textMaxWidth >> 1) - default: - if strings.HasSuffix(opt.Left, "%") { - value, _ := strconv.Atoi(strings.ReplaceAll(opt.Left, "%", "")) - titleX = b.Width() * value / 100 - } else { - value, _ := strconv.Atoi(opt.Left) - titleX = value - } - } - titleY := 0 - // TODO TOP 暂只支持数值 - if opt.Top != "" { - value, _ := strconv.Atoi(opt.Top) - titleY += value - } - for _, item := range measureOptions { - item.style.WriteTextOptionsToRenderer(r) - x := titleX + (textMaxWidth-item.width)>>1 - y := titleY + item.height - d.text(item.text, x, y) - titleY += item.height - } - height := titleY + padding.Top + padding.Bottom - box := padding.Clone() - box.Right = box.Left + titleX + width - box.Bottom = box.Top + height - - return box, nil -} diff --git a/title_test.go b/title_test.go deleted file mode 100644 index 23573c3..0000000 --- a/title_test.go +++ /dev/null @@ -1,142 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -func TestSplitTitleText(t *testing.T) { - assert := assert.New(t) - - assert.Equal([]string{ - "a", - "b", - }, splitTitleText("a\nb")) - assert.Equal([]string{ - "a", - }, splitTitleText("a\n ")) -} - -func TestDrawTitle(t *testing.T) { - assert := assert.New(t) - - newOption := func() *TitleOption { - f, _ := chart.GetDefaultFont() - return &TitleOption{ - Text: "title\nHello", - Subtext: "subtitle\nWorld!", - Style: chart.Style{ - FontSize: 14, - Font: f, - FontColor: drawing.ColorBlack, - }, - SubtextStyle: chart.Style{ - FontSize: 10, - Font: f, - FontColor: drawing.ColorBlue, - }, - } - } - newDraw := func() *Draw { - d, _ := NewDraw(DrawOption{ - Width: 400, - Height: 300, - }) - return d - } - - tests := []struct { - newDraw func() *Draw - newOption func() *TitleOption - result string - box chart.Box - }{ - { - newDraw: newDraw, - newOption: newOption, - result: "\\ntitleHellosubtitleWorld!", - box: chart.Box{ - Right: 43, - Bottom: 58, - }, - }, - { - newDraw: newDraw, - newOption: func() *TitleOption { - opt := newOption() - opt.Left = PositionRight - opt.Top = "50" - return opt - }, - result: "\\ntitleHellosubtitleWorld!", - box: chart.Box{ - Right: 400, - Bottom: 108, - }, - }, - { - newDraw: newDraw, - newOption: func() *TitleOption { - opt := newOption() - opt.Left = PositionCenter - opt.Top = "10" - return opt - }, - result: "\\ntitleHellosubtitleWorld!", - box: chart.Box{ - Right: 222, - Bottom: 68, - }, - }, - { - newDraw: newDraw, - newOption: func() *TitleOption { - opt := newOption() - opt.Left = "10%" - opt.Top = "10" - return opt - }, - result: "\\ntitleHellosubtitleWorld!", - box: chart.Box{ - Right: 83, - Bottom: 68, - }, - }, - } - for _, tt := range tests { - d := tt.newDraw() - o := tt.newOption() - b, err := drawTitle(d, o) - assert.Nil(err) - assert.Equal(tt.box, b) - data, err := d.Bytes() - assert.Nil(err) - assert.NotEmpty(data) - assert.Equal(tt.result, string(data)) - } -} diff --git a/util.go b/util.go index d35b4b0..5fee163 100644 --- a/util.go +++ b/util.go @@ -134,8 +134,8 @@ func commafWithDigits(value float64) string { return humanize.CommafWithDigits(value, decimals) } -func parseColor(color string) drawing.Color { - c := drawing.Color{} +func parseColor(color string) Color { + c := Color{} if color == "" { return c } diff --git a/util_test.go b/util_test.go index 6489ab3..fefbabc 100644 --- a/util_test.go +++ b/util_test.go @@ -80,13 +80,15 @@ func TestGetRadius(t *testing.T) { func TestMeasureTextMaxWidthHeight(t *testing.T) { assert := assert.New(t) - r, err := chart.SVG(400, 300) + p, err := NewPainter(PainterOptions{ + Width: 400, + Height: 300, + }) assert.Nil(err) style := chart.Style{ FontSize: 10, } - style.Font, _ = chart.GetDefaultFont() - style.WriteToRenderer(r) + p.SetStyle(style) maxWidth, maxHeight := measureTextMaxWidthHeight([]string{ "Mon", @@ -96,7 +98,7 @@ func TestMeasureTextMaxWidthHeight(t *testing.T) { "Fri", "Sat", "Sun", - }, r) + }, p) assert.Equal(26, maxWidth) assert.Equal(12, maxHeight) } diff --git a/xaxis.go b/xaxis.go deleted file mode 100644 index d79f40e..0000000 --- a/xaxis.go +++ /dev/null @@ -1,93 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "github.com/golang/freetype/truetype" -) - -type XAxisOption struct { - Font *truetype.Font - // The boundary gap on both sides of a coordinate axis. - // Nil or *true means the center part of two axis ticks - BoundaryGap *bool - // The data value of x axis - Data []string - // The theme of chart - Theme string - // Hidden x axis - Hidden bool - // Number of segments that the axis is split into. Note that this number serves only as a recommendation. - SplitNumber int -} - -func NewXAxisOption(data []string, boundaryGap ...*bool) XAxisOption { - opt := XAxisOption{ - Data: data, - } - if len(boundaryGap) != 0 { - opt.BoundaryGap = boundaryGap[0] - } - return opt -} - -// drawXAxis draws x axis, and returns the height, range of if. -func drawXAxis(p *Painter, opt *XAxisOption, yAxisCount int) (int, *Range, error) { - if opt.Hidden { - return 0, nil, nil - } - left := YAxisWidth - right := (yAxisCount - 1) * YAxisWidth - pXAxis := p.Child( - PainterPaddingOption(Box{ - Left: left, - Right: right, - }), - PainterFontOption(opt.Font), - ) - theme := NewTheme(opt.Theme) - data := NewAxisDataListFromStringList(opt.Data) - style := AxisOption{ - BoundaryGap: opt.BoundaryGap, - StrokeColor: theme.GetAxisStrokeColor(), - FontColor: theme.GetAxisStrokeColor(), - StrokeWidth: 1, - SplitNumber: opt.SplitNumber, - } - - boundary := true - max := float64(len(opt.Data)) - if isFalse(opt.BoundaryGap) { - boundary = false - max-- - } - axis := NewAxis(pXAxis, data, style) - axis.Render() - return axis.measure().Height, &Range{ - divideCount: len(opt.Data), - Min: 0, - Max: max, - Size: pXAxis.Width(), - Boundary: boundary, - }, nil -} diff --git a/xaxis_test.go b/xaxis_test.go deleted file mode 100644 index 267cdb1..0000000 --- a/xaxis_test.go +++ /dev/null @@ -1,108 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestNewXAxisOption(t *testing.T) { - assert := assert.New(t) - - opt := NewXAxisOption([]string{ - "a", - "b", - }, FalseFlag()) - - assert.Equal(XAxisOption{ - Data: []string{ - "a", - "b", - }, - BoundaryGap: FalseFlag(), - }, opt) - -} -func TestDrawXAxis(t *testing.T) { - assert := assert.New(t) - - newDraw := func() *Draw { - d, _ := NewDraw(DrawOption{ - Width: 400, - Height: 300, - }) - return d - } - - tests := []struct { - newDraw func() *Draw - newOption func() *XAxisOption - result string - }{ - { - newDraw: newDraw, - newOption: func() *XAxisOption { - return &XAxisOption{ - BoundaryGap: FalseFlag(), - Data: []string{ - "Mon", - "Tue", - }, - } - }, - result: "\\nMonTue", - }, - { - newDraw: newDraw, - newOption: func() *XAxisOption { - return &XAxisOption{ - Data: []string{ - "01-01", - "01-02", - "01-03", - "01-04", - "01-05", - "01-06", - "01-07", - "01-08", - "01-09", - }, - SplitNumber: 3, - } - }, - result: "\\n01-0201-0501-08", - }, - } - - for _, tt := range tests { - d := tt.newDraw() - height, _, err := drawXAxis(d, tt.newOption(), 1) - assert.Nil(err) - assert.Equal(25, height) - data, err := d.Bytes() - assert.Nil(err) - assert.Equal(tt.result, string(data)) - } -} diff --git a/yaxis.go b/yaxis.go deleted file mode 100644 index 5d55440..0000000 --- a/yaxis.go +++ /dev/null @@ -1,95 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "strings" - - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -type YAxisOption struct { - // The minimun value of axis. - Min *float64 - // The maximum value of axis. - Max *float64 - // Hidden y axis - Hidden bool - // Formatter for y axis text value - Formatter string - // Color for y axis - Color drawing.Color -} - -// TODO 长度是否可以变化 -const YAxisWidth = 40 - -func drawYAxis(p *Painter, opt *ChartOption, axisIndex, xAxisHeight int, padding chart.Box) (*Range, error) { - theme := NewTheme(opt.Theme) - yRange := opt.newYRange(axisIndex) - values := yRange.Values() - yAxis := opt.YAxisList[axisIndex] - formatter := yAxis.Formatter - if len(formatter) != 0 { - for index, text := range values { - values[index] = strings.ReplaceAll(formatter, "{value}", text) - } - } - - data := NewAxisDataListFromStringList(values) - style := AxisOption{ - Position: PositionLeft, - BoundaryGap: FalseFlag(), - FontColor: theme.GetAxisStrokeColor(), - TickShow: FalseFlag(), - StrokeWidth: 1, - SplitLineColor: theme.GetAxisSplitLineColor(), - SplitLineShow: true, - } - if !yAxis.Color.IsZero() { - style.FontColor = yAxis.Color - style.StrokeColor = yAxis.Color - } - width := NewAxis(p, data, style).measure().Width - - yAxisCount := len(opt.YAxisList) - boxWidth := p.Width() - if axisIndex > 0 { - style.SplitLineShow = false - style.Position = PositionRight - padding.Right += (axisIndex - 1) * YAxisWidth - } else { - boxWidth = p.Width() - (yAxisCount-1)*YAxisWidth - padding.Left += (YAxisWidth - width) - } - - pYAxis := p.Child( - PainterWidthHeightOption(boxWidth, p.Height()-xAxisHeight), - PainterPaddingOption(padding), - PainterFontOption(opt.Font), - ) - NewAxis(pYAxis, data, style).Render() - yRange.Size = pYAxis.Height() - return &yRange, nil -} diff --git a/yaxis_test.go b/yaxis_test.go deleted file mode 100644 index 0bbef7a..0000000 --- a/yaxis_test.go +++ /dev/null @@ -1,119 +0,0 @@ -// MIT License - -// Copyright (c) 2022 Tree Xie - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -package charts - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2" -) - -func TestDrawYAxis(t *testing.T) { - assert := assert.New(t) - newDraw := func() *Draw { - d, _ := NewDraw(DrawOption{ - Width: 400, - Height: 300, - }) - return d - } - - tests := []struct { - newDraw func() *Draw - newOption func() *ChartOption - axisIndex int - xAxisHeight int - result string - }{ - { - newDraw: newDraw, - newOption: func() *ChartOption { - return &ChartOption{ - YAxisList: []YAxisOption{ - { - Max: NewFloatPoint(20), - }, - }, - SeriesList: []Series{ - { - Data: []SeriesData{ - { - Value: 1, - }, - { - Value: 2, - }, - }, - }, - }, - } - }, - result: "\\n03.336.661013.3316.6620", - }, - { - newDraw: newDraw, - newOption: func() *ChartOption { - return &ChartOption{ - YAxisList: []YAxisOption{ - {}, - { - Max: NewFloatPoint(20), - Formatter: "{value} C", - }, - }, - SeriesList: []Series{ - { - YAxisIndex: 1, - Data: []SeriesData{ - { - Value: 1, - }, - { - Value: 2, - }, - }, - }, - }, - } - }, - axisIndex: 1, - result: "\\n0 C3.33 C6.66 C10 C13.33 C16.66 C20 C", - }, - } - - for _, tt := range tests { - d := tt.newDraw() - r, err := drawYAxis(d, tt.newOption(), tt.axisIndex, tt.xAxisHeight, chart.NewBox(10, 10, 10, 10)) - assert.Nil(err) - assert.Equal(&Range{ - divideCount: 6, - Max: 20, - Size: 280, - }, r) - - data, err := d.Bytes() - assert.Nil(err) - assert.Equal(tt.result, string(data)) - } -}