diff --git a/axis.go b/axis.go index 8dcabd2..5e33062 100644 --- a/axis.go +++ b/axis.go @@ -1,6 +1,6 @@ // MIT License -// Copyright (c) 2021 Tree Xie +// 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 @@ -23,176 +23,336 @@ package charts import ( - "math" - - "github.com/dustin/go-humanize" + "github.com/golang/freetype/truetype" "github.com/wcharczuk/go-chart/v2" + "github.com/wcharczuk/go-chart/v2/drawing" ) -type ( - // AxisData string - XAxis struct { - // data value of axis - Data []string - // number of segments - SplitNumber int - } -) - -type YAxisOption struct { - // formater of axis - Formater chart.ValueFormatter - // disabled axis - Disabled bool - // min value of axis - Min *float64 - // max value of axis - Max *float64 +type AxisStyle struct { + BoundaryGap *bool + Show *bool + Position string + ClassName string + StrokeColor drawing.Color + StrokeWidth float64 + TickLength int + TickShow *bool + LabelMargin int + FontSize float64 + Font *truetype.Font + FontColor drawing.Color + SplitLineShow bool + SplitLineColor drawing.Color } -const axisStrokeWidth = 1 +func (as *AxisStyle) GetLabelMargin() int { + return getDefaultInt(as.LabelMargin, 8) +} -// GetXAxisAndValues returns x axis by theme, and the values of axis. -func GetXAxisAndValues(xAxis XAxis, tickPosition chart.TickPosition, theme string) (chart.XAxis, []float64) { - data := xAxis.Data - originalSize := len(data) - // 如果居中,则需要多添加一个值 - if tickPosition == chart.TickPositionBetweenTicks { - data = append([]string{ - "", - }, data...) +func (as *AxisStyle) GetTickLength() int { + return getDefaultInt(as.TickLength, 5) +} + +func (as *AxisStyle) 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, } - - size := len(data) - - xValues := make([]float64, size) - ticks := make([]chart.Tick, 0) - - // tick width - maxTicks := maxInt(xAxis.SplitNumber, 10) - - // 计息最多每个unit至少放多个 - minUnitSize := originalSize / maxTicks - if originalSize%maxTicks != 0 { - minUnitSize++ + if s.FontSize == 0 { + s.FontSize = chart.DefaultFontSize } - unitSize := minUnitSize - // 尽可能选择一格展示更多的块 - for i := minUnitSize; i < 2*minUnitSize; i++ { - if originalSize%i == 0 { - unitSize = i + if s.Font == nil { + s.Font = f + } + return s +} + +type AxisData struct { + Text string +} +type AxisDataList []AxisData + +func (l AxisDataList) TextList() []string { + textList := make([]string, len(l)) + for index, item := range l { + textList[index] = item.Text + } + return textList +} + +type axisOption struct { + data *AxisDataList + style *AxisStyle + textMaxWith int + textMaxHeight int + boundaryGap bool +} + +func NewAxisDataListFromStringList(textList []string) AxisDataList { + list := make(AxisDataList, len(textList)) + for index, text := range textList { + list[index] = AxisData{ + Text: text, } } - - for index, key := range data { - f := float64(index) - xValues[index] = f - if index%unitSize == 0 || index == size-1 { - ticks = append(ticks, chart.Tick{ - Value: f, - Label: key, - }) - } - } - return chart.XAxis{ - Ticks: ticks, - TickPosition: tickPosition, - Style: chart.Style{ - FontColor: getAxisColor(theme), - StrokeColor: getAxisColor(theme), - StrokeWidth: axisStrokeWidth, - }, - }, xValues + return list } -func defaultFloatFormater(v interface{}) string { - value, ok := v.(float64) - if !ok { - return "" - } - // 大于10的则直接取整展示 - if value >= 10 { - return humanize.CommafWithDigits(value, 0) - } - return humanize.CommafWithDigits(value, 2) -} - -func newYContinuousRange(option *YAxisOption) *YContinuousRange { - m := YContinuousRange{} - m.Min = -math.MaxFloat64 - m.Max = math.MaxFloat64 - if option != nil { - if option.Min != nil { - m.Min = *option.Min - } - if option.Max != nil { - m.Max = *option.Max - } - } - return &m -} - -// GetSecondaryYAxis returns the secondary y axis by theme -func GetSecondaryYAxis(theme string, option *YAxisOption) chart.YAxis { - strokeColor := getGridColor(theme) - yAxis := chart.YAxis{ - Range: newYContinuousRange(option), - ValueFormatter: defaultFloatFormater, - AxisType: chart.YAxisSecondary, - GridMajorStyle: chart.Style{ - StrokeColor: strokeColor, - StrokeWidth: axisStrokeWidth, - }, - GridMinorStyle: chart.Style{ - StrokeColor: strokeColor, - StrokeWidth: axisStrokeWidth, - }, - Style: chart.Style{ - FontColor: getAxisColor(theme), - // alpha 0,隐藏 - StrokeColor: hiddenColor, - StrokeWidth: axisStrokeWidth, - }, - } - setYAxisOption(&yAxis, option) - return yAxis -} - -func setYAxisOption(yAxis *chart.YAxis, option *YAxisOption) { - if option == nil { +func (d *Draw) axisLabel(opt *axisOption) { + style := opt.style + data := *opt.data + if style.FontColor.IsZero() || len(data) == 0 { return } - if option.Formater != nil { - yAxis.ValueFormatter = option.Formater + r := d.Render + + s := style.Style(d.Font) + s.GetTextOptions().WriteTextOptionsToRenderer(r) + + width := d.Box.Width() + height := d.Box.Height() + textList := data.TextList() + count := len(textList) + boundaryGap := opt.boundaryGap + if !boundaryGap { + count-- + } + + labelMargin := style.GetLabelMargin() + + // 轴线 + labelHeight := labelMargin + opt.textMaxHeight + labelWidth := labelMargin + opt.textMaxWith + + // 坐标轴文本 + position := style.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 := r.MeasureText(text) + if boundaryGap { + height := y - values[index+1] + y -= (height - b.Height()) >> 1 + } else { + y += b.Height() >> 1 + } + // 左右位置的x不一样 + x := width - opt.textMaxWith + if position == PositionLeft { + x = labelWidth - b.Width() - 1 + } + d.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() { + x := values[index] + leftOffset := 0 + b := r.MeasureText(text) + if boundaryGap { + width := values[index+1] - x + leftOffset = (width - b.Width()) >> 1 + } else { + // 左移文本长度 + leftOffset = -b.Width() >> 1 + } + d.text(text, x+leftOffset, y0) + } } } -// GetYAxis returns the primary y axis by theme -func GetYAxis(theme string, option *YAxisOption) chart.YAxis { - disabled := false - if option != nil { - disabled = option.Disabled - } - hidden := chart.Hidden() +func (d *Draw) axisLine(opt *axisOption) { + r := d.Render + style := opt.style + s := style.Style(d.Font) + s.GetStrokeOptions().WriteDrawingOptionsToRenderer(r) - yAxis := chart.YAxis{ - Range: newYContinuousRange(option), - ValueFormatter: defaultFloatFormater, - AxisType: chart.YAxisPrimary, - GridMajorStyle: hidden, - GridMinorStyle: hidden, - Style: chart.Style{ - FontColor: getAxisColor(theme), - // alpha 0,隐藏 - StrokeColor: hiddenColor, - StrokeWidth: axisStrokeWidth, - }, + x0 := 0 + y0 := 0 + x1 := 0 + y1 := 0 + width := d.Box.Width() + height := d.Box.Height() + labelMargin := style.GetLabelMargin() + + // 轴线 + labelHeight := labelMargin + opt.textMaxHeight + labelWidth := labelMargin + opt.textMaxWith + tickLength := style.GetTickLength() + switch style.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 } - // 如果禁用,则默认为隐藏,并设置range - if disabled { - yAxis.Range = &HiddenRange{} - yAxis.Style.Hidden = true - } - setYAxisOption(&yAxis, option) - return yAxis + + d.moveTo(x0, y0) + d.lineTo(x1, y1) + r.FillStroke() +} + +func (d *Draw) axisTick(opt *axisOption) { + r := d.Render + + style := opt.style + s := style.Style(d.Font) + s.GetStrokeOptions().WriteDrawingOptionsToRenderer(r) + + width := d.Box.Width() + height := d.Box.Height() + data := *opt.data + tickCount := len(data) + if !opt.boundaryGap { + tickCount-- + } + labelMargin := style.GetLabelMargin() + + tickLengthValue := style.GetTickLength() + labelHeight := labelMargin + opt.textMaxHeight + labelWidth := labelMargin + opt.textMaxWith + position := style.Position + switch position { + case PositionLeft: + fallthrough + case PositionRight: + values := autoDivide(height, tickCount) + // 左右仅是x0的位置不一样 + x0 := width - labelWidth + if style.Position == PositionLeft { + x0 = labelWidth + } + for _, v := range values { + x := x0 + y := v + d.moveTo(x, y) + d.lineTo(x+tickLengthValue, y) + r.Stroke() + } + // 辅助线 + if style.SplitLineShow && !style.SplitLineColor.IsZero() { + r.SetStrokeColor(style.SplitLineColor) + splitLineWidth := width - labelWidth + if position == PositionRight { + x0 = 0 + splitLineWidth = width - labelWidth - 1 + } + for _, v := range values[0 : len(values)-1] { + x := x0 + y := v + d.moveTo(x, y) + d.lineTo(x+splitLineWidth, y) + r.Stroke() + } + } + default: + values := autoDivide(width, tickCount) + // 上下仅是y0的位置不一样 + y0 := height - labelHeight + if position == PositionTop { + y0 = labelHeight + } + for _, v := range values { + x := v + y := y0 + d.moveTo(x, y-tickLengthValue) + d.lineTo(x, y) + r.Stroke() + } + // 辅助线 + if style.SplitLineShow && !style.SplitLineColor.IsZero() { + r.SetStrokeColor(style.SplitLineColor) + y0 = 0 + splitLineHeight := height - labelHeight - tickLengthValue + if position == PositionTop { + y0 = labelHeight + splitLineHeight = height - labelHeight + } + + for _, v := range values { + x := v + y := y0 + + d.moveTo(x, y) + d.lineTo(x, y0+splitLineHeight) + r.Stroke() + } + } + } +} + +func (d *Draw) axisMeasureTextMaxWidthHeight(data AxisDataList, style AxisStyle) (int, int) { + r := d.Render + s := style.Style(d.Font) + s.GetStrokeOptions().WriteDrawingOptionsToRenderer(r) + s.GetTextOptions().WriteTextOptionsToRenderer(r) + return measureTextMaxWidthHeight(data.TextList(), r) +} + +// measureAxis return the measurement of axis. +// If the position is left or right, it will be textMaxWidth + labelMargin + tickLength. +// If the position is top or bottom, it will be textMaxHeight + labelMargin + tickLength. +func (d *Draw) measureAxis(data AxisDataList, style AxisStyle) int { + value := style.GetLabelMargin() + style.GetTickLength() + textMaxWidth, textMaxHeight := d.axisMeasureTextMaxWidthHeight(data, style) + if style.Position == PositionLeft || + style.Position == PositionRight { + return textMaxWidth + value + } + return textMaxHeight + value +} + +func (d *Draw) Axis(data AxisDataList, style AxisStyle) { + if style.Show != nil && !*style.Show { + return + } + textMaxWidth, textMaxHeight := d.axisMeasureTextMaxWidthHeight(data, style) + opt := &axisOption{ + data: &data, + style: &style, + textMaxWith: textMaxWidth, + textMaxHeight: textMaxHeight, + boundaryGap: true, + } + if style.BoundaryGap != nil && !*style.BoundaryGap { + opt.boundaryGap = false + } + + // 坐标轴线 + d.axisLine(opt) + d.axisTick(opt) + // 坐标文本 + d.axisLabel(opt) } diff --git a/axis_test.go b/axis_test.go index 43779e9..cfbaec4 100644 --- a/axis_test.go +++ b/axis_test.go @@ -1,6 +1,6 @@ // MIT License -// Copyright (c) 2021 Tree Xie +// 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 @@ -23,150 +23,202 @@ package charts import ( - "strconv" "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 TestGetXAxisAndValues(t *testing.T) { +func TestAxisStyle(t *testing.T) { assert := assert.New(t) - genLabels := func(count int) []string { - arr := make([]string, count) - for i := 0; i < count; i++ { - arr[i] = strconv.Itoa(i) - } - return arr - } - genValues := func(count int, betweenTicks bool) []float64 { - if betweenTicks { - count++ - } - arr := make([]float64, count) - for i := 0; i < count; i++ { - arr[i] = float64(i) - } - return arr - } - genTicks := func(count int, betweenTicks bool) []chart.Tick { - arr := make([]chart.Tick, 0) - offset := 0 - if betweenTicks { - offset = 1 - arr = append(arr, chart.Tick{}) - } - for i := 0; i < count; i++ { - arr = append(arr, chart.Tick{ - Value: float64(i + offset), - Label: strconv.Itoa(i), - }) - } - return arr - } + as := AxisStyle{} - tests := []struct { - xAxis XAxis - tickPosition chart.TickPosition - theme string - result chart.XAxis - values []float64 - }{ - { - xAxis: XAxis{ - Data: genLabels(5), - }, - values: genValues(5, false), - result: chart.XAxis{ - Ticks: genTicks(5, false), - }, - }, - // 居中 - { - xAxis: XAxis{ - Data: genLabels(5), - }, - tickPosition: chart.TickPositionBetweenTicks, - // 居中因此value多一个 - values: genValues(5, true), - result: chart.XAxis{ - Ticks: genTicks(5, true), - }, - }, - { - xAxis: XAxis{ - Data: genLabels(20), - }, - // 居中因此value多一个 - values: genValues(20, false), - result: chart.XAxis{ - Ticks: []chart.Tick{ - {Value: 0, Label: "0"}, {Value: 2, Label: "2"}, {Value: 4, Label: "4"}, {Value: 6, Label: "6"}, {Value: 8, Label: "8"}, {Value: 10, Label: "10"}, {Value: 12, Label: "12"}, {Value: 14, Label: "14"}, {Value: 16, Label: "16"}, {Value: 18, Label: "18"}, {Value: 19, Label: "19"}}, - }, - }, - } + assert.Equal(8, as.GetLabelMargin()) + as.LabelMargin = 10 + assert.Equal(10, as.GetLabelMargin()) - for _, tt := range tests { - xAxis, values := GetXAxisAndValues(tt.xAxis, tt.tickPosition, tt.theme) + assert.Equal(5, as.GetTickLength()) + as.TickLength = 6 + assert.Equal(6, as.GetTickLength()) - assert.Equal(tt.result.Ticks, xAxis.Ticks) - assert.Equal(tt.tickPosition, xAxis.TickPosition) - assert.Equal(tt.values, values) - } + f := &truetype.Font{} + style := as.Style(f) + assert.Equal(float64(10), style.FontSize) + assert.Equal(f, style.Font) } -func TestDefaultFloatFormater(t *testing.T) { +func TestAxisDataList(t *testing.T) { assert := assert.New(t) - assert.Equal("", defaultFloatFormater(1)) - - assert.Equal("0.1", defaultFloatFormater(0.1)) - assert.Equal("0.12", defaultFloatFormater(0.123)) - assert.Equal("10", defaultFloatFormater(10.1)) + textList := []string{ + "a", + "b", + } + data := NewAxisDataListFromStringList(textList) + assert.Equal(textList, data.TextList()) } -func TestSetYAxisOption(t *testing.T) { - assert := assert.New(t) - min := 10.0 - max := 20.0 - opt := &YAxisOption{ - Formater: func(v interface{}) string { - return "" - }, - Min: &min, - Max: &max, - } - yAxis := &chart.YAxis{ - Range: newYContinuousRange(opt), - } - setYAxisOption(yAxis, opt) - - assert.NotEmpty(yAxis.ValueFormatter) - assert.Equal(max, yAxis.Range.GetMax()) - assert.Equal(min, yAxis.Range.GetMin()) -} - -func TestGetYAxis(t *testing.T) { +func TestAxis(t *testing.T) { assert := assert.New(t) - yAxis := GetYAxis(ThemeDark, nil) - - assert.True(yAxis.GridMajorStyle.Hidden) - assert.True(yAxis.GridMajorStyle.Hidden) - assert.False(yAxis.Style.Hidden) - - yAxis = GetYAxis(ThemeDark, &YAxisOption{ - Disabled: true, + data := NewAxisDataListFromStringList([]string{ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", }) + getDefaultOption := func() *axisOption { + return &axisOption{ + data: &data, + style: &AxisStyle{ + StrokeColor: drawing.ColorBlack, + StrokeWidth: 1, + FontColor: drawing.ColorBlack, + Show: TrueFlag(), + TickShow: TrueFlag(), + SplitLineShow: true, + SplitLineColor: drawing.ColorBlack.WithAlpha(60), + }, + } + } + tests := []struct { + newOption func() *axisOption + result string + }{ + // 文本按起始位置展示 + // axis位于bottom + { + newOption: func() *axisOption { + opt := getDefaultOption() + opt.style.BoundaryGap = FalseFlag() + return opt + }, + result: "", + }, + // 文本居中展示 + // axis位于bottom + { + newOption: func() *axisOption { + opt := getDefaultOption() + return opt + }, + result: "", + }, + // 文本按起始位置展示 + // axis位于top + { + newOption: func() *axisOption { + opt := getDefaultOption() + opt.style.Position = PositionTop + opt.style.BoundaryGap = FalseFlag() + return opt + }, + result: "", + }, + // 文本居中展示 + // axis位于top + { + newOption: func() *axisOption { + opt := getDefaultOption() + opt.style.Position = PositionTop + return opt + }, + result: "", + }, + // 文本按起始位置展示 + // axis位于left + { + newOption: func() *axisOption { + opt := getDefaultOption() + opt.style.Position = PositionLeft + opt.style.BoundaryGap = FalseFlag() + return opt + }, + result: "", + }, + // 文本居中展示 + // axis位于left + { + newOption: func() *axisOption { + opt := getDefaultOption() + opt.style.Position = PositionLeft + return opt + }, + result: "", + }, + // 文本按起始位置展示 + // axis位于right + { + newOption: func() *axisOption { + opt := getDefaultOption() + opt.style.Position = PositionRight + opt.style.BoundaryGap = FalseFlag() + return opt + }, + result: "", + }, + // 文本居中展示 + // axis位于right + { + newOption: func() *axisOption { + opt := getDefaultOption() + opt.style.Position = PositionRight + return opt + }, + result: "", + }, + } + for _, tt := range tests { + d, err := NewDraw(DrawOption{ + Width: 400, + Height: 300, + }, PaddingOption(chart.Box{ + Left: 5, + Top: 5, + Right: 5, + Bottom: 5, + })) + assert.Nil(err) + opt := tt.newOption() - assert.True(yAxis.GridMajorStyle.Hidden) - assert.True(yAxis.GridMajorStyle.Hidden) - assert.True(yAxis.Style.Hidden) + d.Axis(*opt.data, *opt.style) - // secondary yAxis - yAxis = GetSecondaryYAxis(ThemeDark, nil) - assert.False(yAxis.GridMajorStyle.Hidden) - assert.False(yAxis.GridMajorStyle.Hidden) - assert.True(yAxis.Style.StrokeColor.IsTransparent()) + result, err := d.Bytes() + assert.Nil(err) + assert.Equal(tt.result, string(result)) + } +} + +func TestMeasureAxis(t *testing.T) { + assert := assert.New(t) + + d, err := NewDraw(DrawOption{ + Width: 400, + Height: 300, + }) + assert.Nil(err) + data := NewAxisDataListFromStringList([]string{ + "Mon", + "Sun", + }) + f, _ := chart.GetDefaultFont() + width := d.measureAxis(data, AxisStyle{ + FontSize: 12, + Font: f, + Position: PositionLeft, + }) + assert.Equal(44, width) + + height := d.measureAxis(data, AxisStyle{ + FontSize: 12, + Font: f, + Position: PositionTop, + }) + assert.Equal(28, height) } diff --git a/line_series.go b/bar.go similarity index 62% rename from line_series.go rename to bar.go index 93a1479..eb88cb1 100644 --- a/line_series.go +++ b/bar.go @@ -1,6 +1,6 @@ // MIT License -// Copyright (c) 2021 Tree Xie +// 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 @@ -24,26 +24,34 @@ package charts import ( "github.com/wcharczuk/go-chart/v2" + "github.com/wcharczuk/go-chart/v2/drawing" ) -type LineSeries struct { - BaseSeries +type BarStyle struct { + ClassName string + StrokeDashArray []float64 + FillColor drawing.Color } -func (ls LineSeries) getXRange(xrange chart.Range) chart.Range { - if ls.TickPosition != chart.TickPositionBetweenTicks { - return xrange +func (bs *BarStyle) Style() chart.Style { + return chart.Style{ + ClassName: bs.ClassName, + StrokeDashArray: bs.StrokeDashArray, + StrokeColor: bs.FillColor, + StrokeWidth: 1, + FillColor: bs.FillColor, } - // 如果是居中,画线时重新调整 - return wrapRange(xrange, ls.TickPosition) } -func (ls LineSeries) Render(r chart.Renderer, canvasBox chart.Box, xrange, yrange chart.Range, defaults chart.Style) { - style := ls.Style.InheritFrom(defaults) - xrange = ls.getXRange(xrange) - chart.Draw.LineSeries(r, canvasBox, xrange, yrange, style, ls) - lr := LabelRenderer{ - Options: ls.Label, - } - lr.Render(r, canvasBox, xrange, yrange, style, ls) +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_series.go b/bar_series.go deleted file mode 100644 index b9e8fc1..0000000 --- a/bar_series.go +++ /dev/null @@ -1,148 +0,0 @@ -// MIT License - -// Copyright (c) 2021 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/dustin/go-humanize" - "github.com/wcharczuk/go-chart/v2" -) - -const defaultBarMargin = 10 - -type BarSeriesCustomStyle struct { - PointIndex int - Index int - Style chart.Style -} - -type BarSeries struct { - BaseSeries - Count int - Index int - // 间隔 - Margin int - // 偏移量 - Offset int - // 宽度 - BarWidth int - CustomStyles []BarSeriesCustomStyle -} - -type barSeriesWidthValues struct { - columnWidth int - columnMargin int - margin int - barWidth int -} - -func (bs BarSeries) GetBarStyle(index, pointIndex int) chart.Style { - // 指定样式 - for _, item := range bs.CustomStyles { - if item.Index == index && item.PointIndex == pointIndex { - return item.Style - } - } - // 其它非指定样式 - return chart.Style{} -} - -func (bs BarSeries) getWidthValues(width int) barSeriesWidthValues { - columnWidth := width / bs.Len() - // 块间隔 - columnMargin := columnWidth / 10 - minColumnMargin := 2 - if columnMargin < minColumnMargin { - columnMargin = minColumnMargin - } - margin := bs.Margin - if margin <= 0 { - margin = defaultBarMargin - } - // 如果margin大于column margin - if margin > columnMargin { - margin = columnMargin - } - - allBarMarginWidth := (bs.Count - 1) * margin - barWidth := ((columnWidth - 2*columnMargin) - allBarMarginWidth) / bs.Count - if bs.BarWidth > 0 && bs.BarWidth < barWidth { - barWidth = bs.BarWidth - // 重新计息columnMargin - columnMargin = (columnWidth - allBarMarginWidth - (bs.Count * barWidth)) / 2 - } - return barSeriesWidthValues{ - columnWidth: columnWidth, - columnMargin: columnMargin, - margin: margin, - barWidth: barWidth, - } -} - -func (bs BarSeries) Render(r chart.Renderer, canvasBox chart.Box, xrange, yrange chart.Range, defaults chart.Style) { - if bs.Len() == 0 || bs.Count <= 0 { - return - } - style := bs.Style.InheritFrom(defaults) - style.FillColor = style.StrokeColor - if !style.ShouldDrawStroke() { - return - } - - cb := canvasBox.Bottom - cl := canvasBox.Left - widthValues := bs.getWidthValues(canvasBox.Width()) - labelValues := make([]LabelValue, 0) - - for i := 0; i < bs.Len(); i++ { - vx, vy := bs.GetValues(i) - customStyle := bs.GetBarStyle(bs.Index, i) - cloneStyle := style - if !customStyle.IsZero() { - cloneStyle.FillColor = customStyle.FillColor - cloneStyle.StrokeColor = customStyle.StrokeColor - } - - x := cl + xrange.Translate(vx) - // 由于bar是居中展示,因此需要往前移一个显示块 - x += (-widthValues.columnWidth + widthValues.columnMargin) - // 计算是第几个bar,位置右偏 - x += bs.Index * (widthValues.margin + widthValues.barWidth) - y := cb - yrange.Translate(vy) - - chart.Draw.Box(r, chart.Box{ - Left: x, - Top: y, - Right: x + widthValues.barWidth, - Bottom: canvasBox.Bottom - 1, - }, cloneStyle) - labelValues = append(labelValues, LabelValue{ - Left: x + widthValues.barWidth/2, - Top: y, - Text: humanize.CommafWithDigits(vy, 2), - }) - } - lr := LabelRenderer{ - Options: bs.Label, - } - lr.CustomizeRender(r, style, labelValues) -} diff --git a/bar_series_test.go b/bar_series_test.go deleted file mode 100644 index 8703367..0000000 --- a/bar_series_test.go +++ /dev/null @@ -1,166 +0,0 @@ -// MIT License - -// Copyright (c) 2021 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" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -func TestBarSeries(t *testing.T) { - assert := assert.New(t) - - customStyle := chart.Style{ - StrokeColor: drawing.ColorBlue, - } - bs := BarSeries{ - CustomStyles: []BarSeriesCustomStyle{ - { - PointIndex: 1, - Style: customStyle, - }, - }, - } - - assert.Equal(customStyle, bs.GetBarStyle(0, 1)) - - assert.True(bs.GetBarStyle(1, 0).IsZero()) -} - -func TestBarSeriesGetWidthValues(t *testing.T) { - assert := assert.New(t) - - bs := BarSeries{ - Count: 1, - BaseSeries: BaseSeries{ - XValues: []float64{ - 1, - 2, - 3, - }, - }, - } - widthValues := bs.getWidthValues(300) - assert.Equal(barSeriesWidthValues{ - columnWidth: 100, - columnMargin: 10, - margin: 10, - barWidth: 80, - }, widthValues) - - // 指定margin - bs.Margin = 5 - widthValues = bs.getWidthValues(300) - assert.Equal(barSeriesWidthValues{ - columnWidth: 100, - columnMargin: 10, - margin: 5, - barWidth: 80, - }, widthValues) - - // 指定bar的宽度 - bs.BarWidth = 60 - widthValues = bs.getWidthValues(300) - assert.Equal(barSeriesWidthValues{ - columnWidth: 100, - columnMargin: 20, - margin: 5, - barWidth: 60, - }, widthValues) -} - -func TestBarSeriesRender(t *testing.T) { - assert := assert.New(t) - - width := 800 - height := 400 - - r, err := chart.SVG(width, height) - assert.Nil(err) - - bs := BarSeries{ - Count: 1, - CustomStyles: []BarSeriesCustomStyle{ - { - Index: 0, - PointIndex: 1, - Style: chart.Style{ - StrokeColor: SeriesColorsLight[1], - }, - }, - }, - BaseSeries: BaseSeries{ - TickPosition: chart.TickPositionBetweenTicks, - Style: chart.Style{ - StrokeColor: SeriesColorsLight[0], - StrokeWidth: 1, - }, - XValues: []float64{ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - }, - YValues: []float64{ - // 第一个点为占位点 - 0, - 120, - 200, - 150, - 80, - 70, - 110, - 130, - }, - }, - } - xrange := &chart.ContinuousRange{ - Min: 0, - Max: 7, - Domain: 753, - } - yrange := &chart.ContinuousRange{ - Min: 70, - Max: 200, - Domain: 362, - } - bs.Render(r, chart.Box{ - Top: 11, - Left: 42, - Right: 795, - Bottom: 373, - }, xrange, yrange, chart.Style{}) - - buffer := bytes.Buffer{} - err = r.Save(&buffer) - assert.Nil(err) - assert.Equal("", buffer.String()) -} diff --git a/range_test.go b/bar_test.go similarity index 54% rename from range_test.go rename to bar_test.go index 33937bf..01b6d3c 100644 --- a/range_test.go +++ b/bar_test.go @@ -1,6 +1,6 @@ // MIT License -// Copyright (c) 2021 Tree Xie +// 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 @@ -23,55 +23,56 @@ package charts import ( - "math" "testing" "github.com/stretchr/testify/assert" "github.com/wcharczuk/go-chart/v2" + "github.com/wcharczuk/go-chart/v2/drawing" ) -func TestRange(t *testing.T) { +func TestBarStyle(t *testing.T) { assert := assert.New(t) - r := Range{ - ContinuousRange: chart.ContinuousRange{ - Min: 0, - Max: 5, - Domain: 500, + bs := BarStyle{ + ClassName: "test", + StrokeDashArray: []float64{ + 1.0, }, + FillColor: drawing.ColorBlack, } - assert.Equal(100, r.Translate(1)) - - r.TickPosition = chart.TickPositionBetweenTicks - assert.Equal(50, r.Translate(1)) + assert.Equal(chart.Style{ + ClassName: "test", + StrokeDashArray: []float64{ + 1.0, + }, + StrokeWidth: 1, + FillColor: drawing.ColorBlack, + StrokeColor: drawing.ColorBlack, + }, bs.Style()) } -func TestHiddenRange(t *testing.T) { +func TestDrawBar(t *testing.T) { assert := assert.New(t) - r := HiddenRange{} - - assert.Equal(float64(0), r.GetDelta()) -} - -func TestYContinuousRange(t *testing.T) { - assert := assert.New(t) - r := YContinuousRange{} - r.Min = -math.MaxFloat64 - r.Max = math.MaxFloat64 - - assert.True(r.IsZero()) - - r.SetMin(1.0) - assert.Equal(1.0, r.GetMin()) - // 再次设置无效 - r.SetMin(2.0) - assert.Equal(1.0, r.GetMin()) - - r.SetMax(5.0) - // *1.2 - assert.Equal(6.0, r.GetMax()) - // 再次设置无效 - r.SetMax(10.0) - assert.Equal(6.0, r.GetMax()) + 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("", string(data)) } diff --git a/base_series.go b/base_series.go deleted file mode 100644 index 37e3689..0000000 --- a/base_series.go +++ /dev/null @@ -1,140 +0,0 @@ -// MIT License - -// Copyright (c) 2021 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" - - "github.com/wcharczuk/go-chart/v2" -) - -// Interface Assertions. -var ( - _ chart.Series = (*BaseSeries)(nil) - _ chart.FirstValuesProvider = (*BaseSeries)(nil) - _ chart.LastValuesProvider = (*BaseSeries)(nil) -) - -type SeriesLabel struct { - Show bool - Offset chart.Box -} - -// BaseSeries represents a line on a chart. -type BaseSeries struct { - Name string - Style chart.Style - TickPosition chart.TickPosition - - YAxis chart.YAxisType - - XValueFormatter chart.ValueFormatter - YValueFormatter chart.ValueFormatter - - XValues []float64 - YValues []float64 - - Label SeriesLabel -} - -// GetName returns the name of the time series. -func (bs BaseSeries) GetName() string { - return bs.Name -} - -// GetStyle returns the line style. -func (bs BaseSeries) GetStyle() chart.Style { - return bs.Style -} - -// Len returns the number of elements in the series. -func (bs BaseSeries) Len() int { - offset := 0 - if bs.TickPosition == chart.TickPositionBetweenTicks { - offset = -1 - } - return len(bs.XValues) + offset -} - -// GetValues gets the x,y values at a given index. -func (bs BaseSeries) GetValues(index int) (float64, float64) { - if bs.TickPosition == chart.TickPositionBetweenTicks { - index++ - } - return bs.XValues[index], bs.YValues[index] -} - -// GetFirstValues gets the first x,y values. -func (bs BaseSeries) GetFirstValues() (float64, float64) { - index := 0 - if bs.TickPosition == chart.TickPositionBetweenTicks { - index++ - } - return bs.XValues[index], bs.YValues[index] -} - -// GetLastValues gets the last x,y values. -func (bs BaseSeries) GetLastValues() (float64, float64) { - return bs.XValues[len(bs.XValues)-1], bs.YValues[len(bs.YValues)-1] -} - -// GetValueFormatters returns value formatter defaults for the series. -func (bs BaseSeries) GetValueFormatters() (x, y chart.ValueFormatter) { - if bs.XValueFormatter != nil { - x = bs.XValueFormatter - } else { - x = chart.FloatValueFormatter - } - if bs.YValueFormatter != nil { - y = bs.YValueFormatter - } else { - y = chart.FloatValueFormatter - } - return -} - -// GetYAxis returns which YAxis the series draws on. -func (bs BaseSeries) GetYAxis() chart.YAxisType { - return bs.YAxis -} - -// Render renders the series. -func (bs BaseSeries) Render(r chart.Renderer, canvasBox chart.Box, xrange, yrange chart.Range, defaults chart.Style) { - fmt.Println("should be override the function") -} - -// Validate validates the series. -func (bs BaseSeries) Validate() error { - if len(bs.XValues) == 0 { - return fmt.Errorf("continuous series; must have xvalues set") - } - - if len(bs.YValues) == 0 { - return fmt.Errorf("continuous series; must have yvalues set") - } - - if len(bs.XValues) != len(bs.YValues) { - return fmt.Errorf("continuous series; must have same length xvalues as yvalues") - } - return nil -} diff --git a/base_series_test.go b/base_series_test.go deleted file mode 100644 index 0c9b5d1..0000000 --- a/base_series_test.go +++ /dev/null @@ -1,94 +0,0 @@ -// MIT License - -// Copyright (c) 2021 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 ( - "reflect" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/wcharczuk/go-chart/v2" -) - -func TestBaseSeries(t *testing.T) { - assert := assert.New(t) - - bs := BaseSeries{ - XValues: []float64{ - 1, - 2, - 3, - }, - YValues: []float64{ - 10, - 20, - 30, - }, - } - assert.Equal(3, bs.Len()) - bs.TickPosition = chart.TickPositionBetweenTicks - assert.Equal(2, bs.Len()) - - bs.TickPosition = chart.TickPositionUnset - x, y := bs.GetValues(1) - assert.Equal(float64(2), x) - assert.Equal(float64(20), y) - bs.TickPosition = chart.TickPositionBetweenTicks - x, y = bs.GetValues(1) - assert.Equal(float64(3), x) - assert.Equal(float64(30), y) - - bs.TickPosition = chart.TickPositionUnset - x, y = bs.GetFirstValues() - assert.Equal(float64(1), x) - assert.Equal(float64(10), y) - bs.TickPosition = chart.TickPositionBetweenTicks - x, y = bs.GetFirstValues() - assert.Equal(float64(2), x) - assert.Equal(float64(20), y) - - bs.TickPosition = chart.TickPositionUnset - x, y = bs.GetLastValues() - assert.Equal(float64(3), x) - assert.Equal(float64(30), y) - bs.TickPosition = chart.TickPositionBetweenTicks - x, y = bs.GetLastValues() - assert.Equal(float64(3), x) - assert.Equal(float64(30), y) - - xFormater, yFormater := bs.GetValueFormatters() - assert.Equal(reflect.ValueOf(chart.FloatValueFormatter).Pointer(), reflect.ValueOf(xFormater).Pointer()) - assert.Equal(reflect.ValueOf(chart.FloatValueFormatter).Pointer(), reflect.ValueOf(yFormater).Pointer()) - formater := func(v interface{}) string { - return "" - } - bs.XValueFormatter = formater - bs.YValueFormatter = formater - xFormater, yFormater = bs.GetValueFormatters() - assert.Equal(reflect.ValueOf(formater).Pointer(), reflect.ValueOf(xFormater).Pointer()) - assert.Equal(reflect.ValueOf(formater).Pointer(), reflect.ValueOf(yFormater).Pointer()) - - assert.Equal(chart.YAxisPrimary, bs.GetYAxis()) - - assert.Nil(bs.Validate()) -} diff --git a/chart.go b/chart.go new file mode 100644 index 0000000..b768993 --- /dev/null +++ b/chart.go @@ -0,0 +1,145 @@ +// 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/dustin/go-humanize" + "github.com/wcharczuk/go-chart/v2" + "github.com/wcharczuk/go-chart/v2/drawing" +) + +type XAxisOption struct { + BoundaryGap *bool + Data []string + // TODO split number +} + +type SeriesData struct { + Value float64 + Style chart.Style +} +type Point struct { + X int + Y int +} + +type Range struct { + originalMin float64 + originalMax float64 + divideCount int + Min float64 + Max float64 + Size int + Boundary bool +} + +func (r *Range) getHeight(value float64) int { + v := 1 - value/(r.Max-r.Min) + return int(v * float64(r.Size)) +} + +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)) +} + +type Series struct { + Type string + Name string + Data []SeriesData + YAxisIndex int + Style chart.Style +} + +type ChartOption struct { + Theme string + XAxis XAxisOption + Width int + Height int + Parent *Draw + Padding chart.Box + SeriesList []Series + BackgroundColor drawing.Color +} + +func (o *ChartOption) getWidth() int { + if o.Width == 0 { + return 600 + } + return o.Width +} + +func (o *ChartOption) getHeight() int { + if o.Height == 0 { + return 400 + } + return o.Height +} + +func (o *ChartOption) getYRange(axisIndex int) Range { + min := float64(0) + max := float64(0) + + 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 + } + } + } + // TODO 对于小数的处理 + + divideCount := 6 + r := Range{ + originalMin: min, + originalMax: max, + Min: float64(int(min * 0.8)), + Max: max * 1.2, + divideCount: divideCount, + } + value := int((r.Max - r.Min) / float64(divideCount)) + r.Max = float64(int(float64(value*divideCount) + r.Min)) + return r +} + +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 := humanize.CommafWithDigits(v, 2) + values = append(values, value) + } + return values +} diff --git a/charts.go b/charts.go deleted file mode 100644 index 5957fdb..0000000 --- a/charts.go +++ /dev/null @@ -1,292 +0,0 @@ -// MIT License - -// Copyright (c) 2021 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" - "io" - "sync" - - "github.com/dustin/go-humanize" - "github.com/golang/freetype/truetype" - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -const ( - ThemeLight = "light" - ThemeDark = "dark" -) - -const ( - DefaultChartWidth = 800 - DefaultChartHeight = 400 -) - -type ( - Title struct { - Text string - Style chart.Style - Font *truetype.Font - Left string - Top string - } - Legend struct { - Data []string - Align string - Padding chart.Box - Left string - Right string - Top string - Bottom string - } - Options struct { - Padding chart.Box - Width int - Height int - Theme string - XAxis XAxis - YAxisOptions []*YAxisOption - Series []Series - Title Title - Legend Legend - TickPosition chart.TickPosition - Log chart.Logger - Font *truetype.Font - } -) - -var fonts = sync.Map{} -var ErrFontNotExists = errors.New("font is not exists") - -// InstallFont installs the font for charts -func InstallFont(fontFamily string, data []byte) error { - font, err := truetype.Parse(data) - if err != nil { - return err - } - fonts.Store(fontFamily, font) - return nil -} - -// GetFont returns the font of font family -func GetFont(fontFamily string) (*truetype.Font, error) { - value, ok := fonts.Load(fontFamily) - if !ok { - return nil, ErrFontNotExists - } - f, ok := value.(*truetype.Font) - if !ok { - return nil, ErrFontNotExists - } - return f, nil -} - -type Graph interface { - Render(rp chart.RendererProvider, w io.Writer) error -} - -func (o *Options) validate() error { - if len(o.Series) == 0 { - return errors.New("series can not be empty") - } - xAxisCount := len(o.XAxis.Data) - - for _, item := range o.Series { - if item.Type != SeriesPie && len(item.Data) != xAxisCount { - return errors.New("series and xAxis is not matched") - } - } - return nil -} - -func (o *Options) getWidth() int { - width := o.Width - if width <= 0 { - width = DefaultChartWidth - } - return width -} - -func (o *Options) getHeight() int { - height := o.Height - if height <= 0 { - height = DefaultChartHeight - } - return height -} - -func (o *Options) getBackground() chart.Style { - bg := chart.Style{ - Padding: o.Padding, - } - return bg -} - -func render(g Graph, rp chart.RendererProvider) ([]byte, error) { - buf := bytes.Buffer{} - err := g.Render(rp, &buf) - if err != nil { - return nil, err - } - return buf.Bytes(), nil -} - -func ToPNG(g Graph) ([]byte, error) { - return render(g, chart.PNG) -} - -func ToSVG(g Graph) ([]byte, error) { - return render(g, chart.SVG) -} - -func newTitleRenderable(title Title, font *truetype.Font, textColor drawing.Color) chart.Renderable { - if title.Text == "" || title.Style.Hidden { - return nil - } - title.Font = font - if title.Style.FontColor.IsZero() { - title.Style.FontColor = textColor - } - return NewTitleCustomize(title) -} - -func newPieChart(opt Options) *chart.PieChart { - values := make(chart.Values, len(opt.Series)) - for index, item := range opt.Series { - label := item.Name - if item.Label.Show { - label += ":" + humanize.CommafWithDigits(item.Data[0].Value, 2) - } - values[index] = chart.Value{ - Value: item.Data[0].Value, - Label: label, - } - } - p := &chart.PieChart{ - Font: opt.Font, - Background: opt.getBackground(), - Width: opt.getWidth(), - Height: opt.getHeight(), - Values: values, - ColorPalette: &PieThemeColorPalette{ - ThemeColorPalette: ThemeColorPalette{ - Theme: opt.Theme, - }, - }, - } - // pie 图表默认设置为居中 - if opt.Title.Left == "" { - opt.Title.Left = "center" - } - titleColor := drawing.ColorBlack - if opt.Theme == ThemeDark { - titleColor = drawing.ColorWhite - } - titleRender := newTitleRenderable(opt.Title, p.GetFont(), titleColor) - if titleRender != nil { - p.Elements = []chart.Renderable{ - titleRender, - } - } - return p -} - -func newChart(opt Options) *chart.Chart { - tickPosition := opt.TickPosition - - xAxis, xValues := GetXAxisAndValues(opt.XAxis, tickPosition, opt.Theme) - - legendSize := len(opt.Legend.Data) - for index, item := range opt.Series { - if len(item.XValues) == 0 { - opt.Series[index].XValues = xValues - } - if index < legendSize && opt.Series[index].Name == "" { - opt.Series[index].Name = opt.Legend.Data[index] - } - } - - var secondaryYAxisOption *YAxisOption - if len(opt.YAxisOptions) != 0 { - secondaryYAxisOption = opt.YAxisOptions[0] - } - - yAxisOption := &YAxisOption{ - Disabled: true, - } - if len(opt.YAxisOptions) > 1 { - yAxisOption = opt.YAxisOptions[1] - } - - c := &chart.Chart{ - Font: opt.Font, - Log: opt.Log, - Background: opt.getBackground(), - ColorPalette: &ThemeColorPalette{ - Theme: opt.Theme, - }, - Width: opt.getWidth(), - Height: opt.getHeight(), - XAxis: xAxis, - YAxis: GetYAxis(opt.Theme, yAxisOption), - YAxisSecondary: GetSecondaryYAxis(opt.Theme, secondaryYAxisOption), - Series: GetSeries(opt.Series, tickPosition, opt.Theme), - } - - elements := make([]chart.Renderable, 0) - - if legendSize != 0 { - elements = append(elements, NewLegendCustomize(c.Series, LegendOption{ - Theme: opt.Theme, - IconDraw: DefaultLegendIconDraw, - Align: opt.Legend.Align, - Padding: opt.Legend.Padding, - Left: opt.Legend.Left, - Right: opt.Legend.Right, - Top: opt.Legend.Top, - Bottom: opt.Legend.Bottom, - })) - } - titleRender := newTitleRenderable(opt.Title, c.GetFont(), c.GetColorPalette().TextColor()) - if titleRender != nil { - elements = append(elements, titleRender) - } - if len(elements) != 0 { - c.Elements = elements - } - return c -} - -func New(opt Options) (Graph, error) { - err := opt.validate() - if err != nil { - return nil, err - } - if opt.Series[0].Type == SeriesPie { - return newPieChart(opt), nil - } - - return newChart(opt), nil -} diff --git a/charts_test.go b/charts_test.go deleted file mode 100644 index 98a7288..0000000 --- a/charts_test.go +++ /dev/null @@ -1,156 +0,0 @@ -// MIT License - -// Copyright (c) 2021 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/roboto" -) - -func TestFont(t *testing.T) { - assert := assert.New(t) - - fontFamily := "roboto" - err := InstallFont(fontFamily, roboto.Roboto) - assert.Nil(err) - - font, err := GetFont(fontFamily) - assert.Nil(err) - assert.NotNil(font) -} - -func TestChartsOptions(t *testing.T) { - assert := assert.New(t) - - o := Options{} - - assert.Equal(errors.New("series can not be empty"), o.validate()) - - o.Series = []Series{ - { - Data: []SeriesData{ - { - Value: 1, - }, - }, - }, - } - assert.Equal(errors.New("series and xAxis is not matched"), o.validate()) - o.XAxis.Data = []string{ - "1", - } - assert.Nil(o.validate()) - - assert.Equal(DefaultChartWidth, o.getWidth()) - o.Width = 10 - assert.Equal(10, o.getWidth()) - - assert.Equal(DefaultChartHeight, o.getHeight()) - o.Height = 10 - assert.Equal(10, o.getHeight()) - - padding := chart.NewBox(10, 10, 10, 10) - o.Padding = padding - assert.Equal(padding, o.getBackground().Padding) -} - -func TestNewPieChart(t *testing.T) { - assert := assert.New(t) - - data := []Series{ - { - Data: []SeriesData{ - { - Value: 10, - }, - }, - Name: "chrome", - }, - { - Data: []SeriesData{ - { - Value: 2, - }, - }, - Name: "edge", - }, - } - pie := newPieChart(Options{ - Series: data, - }) - for index, item := range pie.Values { - assert.Equal(data[index].Name, item.Label) - assert.Equal(data[index].Data[0].Value, item.Value) - } -} - -func TestNewChart(t *testing.T) { - assert := assert.New(t) - - data := []Series{ - { - Data: []SeriesData{ - { - Value: 10, - }, - { - Value: 20, - }, - }, - Name: "chrome", - }, - { - Data: []SeriesData{ - { - Value: 2, - }, - { - Value: 3, - }, - }, - Name: "edge", - }, - } - - c := newChart(Options{ - Series: data, - }) - assert.Empty(c.Elements) - for index, series := range c.Series { - assert.Equal(data[index].Name, series.GetName()) - } - - c = newChart(Options{ - Legend: Legend{ - Data: []string{ - "chrome", - "edge", - }, - }, - }) - assert.Equal(1, len(c.Elements)) -} diff --git a/draw.go b/draw.go index 7b9dc56..e7c37a4 100644 --- a/draw.go +++ b/draw.go @@ -24,6 +24,7 @@ package charts import ( "bytes" + "errors" "github.com/golang/freetype/truetype" "github.com/wcharczuk/go-chart/v2" @@ -37,152 +38,78 @@ const ( PositionBottom = "bottom" ) -type draw struct { +type Draw struct { Render chart.Renderer Box chart.Box - parent *draw + Font *truetype.Font + parent *Draw } -type Point struct { - X int - Y int +type DrawOption struct { + Type string + Parent *Draw + Width int + Height int } -type LineStyle struct { - ClassName string - StrokeDashArray []float64 - StrokeColor drawing.Color - StrokeWidth float64 - FillColor drawing.Color - DotWidth float64 - DotColor drawing.Color - DotFillColor drawing.Color -} +type Option func(*Draw) error -func (ls *LineStyle) Style() chart.Style { - return chart.Style{ - ClassName: ls.ClassName, - StrokeDashArray: ls.StrokeDashArray, - StrokeColor: ls.StrokeColor, - StrokeWidth: ls.StrokeWidth, - FillColor: ls.FillColor, - DotWidth: ls.DotWidth, - DotColor: ls.DotColor, +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 } } -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, +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") } -} - -type AxisStyle struct { - BoundaryGap *bool - Show *bool - Position string - Offset int - ClassName string - StrokeColor drawing.Color - StrokeWidth float64 - TickLength int - TickShow *bool - LabelMargin int - FontSize float64 - Font *truetype.Font - FontColor drawing.Color -} - -func (as *AxisStyle) GetLabelMargin() int { - return getDefaultInt(as.LabelMargin, 8) -} - -func (as *AxisStyle) GetTickLength() int { - return getDefaultInt(as.TickLength, 5) -} - -func (as *AxisStyle) Style() chart.Style { - s := chart.Style{ - ClassName: as.ClassName, - StrokeColor: as.StrokeColor, - StrokeWidth: as.StrokeWidth, - FontSize: as.FontSize, - FontColor: as.FontColor, - Font: as.Font, + font, _ := chart.GetDefaultFont() + d := &Draw{ + Font: font, } - if s.FontSize == 0 { - s.FontSize = chart.DefaultFontSize + 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 s.Font == nil { - s.Font, _ = chart.GetDefaultFont() + if width != 0 && height != 0 { + d.Box.Right = width + d.Box.Left + d.Box.Bottom = height + d.Box.Top } - return s -} - -type AxisData struct { - Text string -} -type AxisDataList []AxisData - -func (l AxisDataList) TextList() []string { - textList := make([]string, len(l)) - for index, item := range l { - textList[index] = item.Text + // 创建render + if d.parent == nil { + fn := chart.SVG + if opt.Type == "png" { + fn = chart.PNG + } + r, err := fn(d.Box.Right, d.Box.Bottom) + if err != nil { + return nil, err + } + d.Render = r } - return textList -} -type axisOption struct { - data *AxisDataList - style *AxisStyle - textMaxWith int - textMaxHeight int -} - -func NewAxisDataListFromStringList(textList []string) AxisDataList { - list := make(AxisDataList, len(textList)) - for index, text := range textList { - list[index] = AxisData{ - Text: text, + for _, o := range opts { + err := o(d) + if err != nil { + return nil, err } } - return list + return d, nil } -type Option func(*draw) - -func ParentOption(p *draw) Option { - return func(d *draw) { - d.parent = p - } -} - -func NewDraw(r chart.Renderer, box chart.Box, opts ...Option) *draw { - d := &draw{ - Render: r, - Box: box, - } - for _, opt := range opts { - opt(d) - } - return d -} - -func (d *draw) Parent() *draw { +func (d *Draw) Parent() *Draw { return d.parent } -func (d *draw) Top() *draw { +func (d *Draw) Top() *Draw { if d.parent == nil { return nil } @@ -197,7 +124,7 @@ func (d *draw) Top() *draw { return t } -func (d *draw) Bytes() ([]byte, error) { +func (d *Draw) Bytes() ([]byte, error) { buffer := bytes.Buffer{} err := d.Render.Save(&buffer) if err != nil { @@ -206,23 +133,23 @@ func (d *draw) Bytes() ([]byte, error) { return buffer.Bytes(), err } -func (d *draw) moveTo(x, y int) { +func (d *Draw) moveTo(x, y int) { d.Render.MoveTo(x+d.Box.Left, y+d.Box.Top) } -func (d *draw) lineTo(x, y int) { +func (d *Draw) lineTo(x, y int) { d.Render.LineTo(x+d.Box.Left, y+d.Box.Top) } -func (d *draw) circle(radius float64, x, y int) { +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) { +func (d *Draw) text(body string, x, y int) { d.Render.Text(body, x+d.Box.Left, y+d.Box.Top) } -func (d *draw) lineStroke(points []Point, style LineStyle) { +func (d *Draw) lineStroke(points []Point, style LineStyle) { s := style.Style() if !s.ShouldDrawStroke() { return @@ -241,290 +168,16 @@ func (d *draw) lineStroke(points []Point, style LineStyle) { r.Stroke() } -func (d *draw) lineFill(points []Point, style LineStyle) { - s := style.Style() - if !(s.ShouldDrawStroke() && s.ShouldDrawFill()) { - return - } +func (d *Draw) setBackground(width, height int, color drawing.Color) { 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) - } + s := chart.Style{ + FillColor: color, } - height := d.Box.Height() - d.lineTo(x, height) - x0 := points[0].X - y0 := points[0].Y - d.lineTo(x0, height) - d.lineTo(x0, y0) - r.Fill() -} - -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) -} - -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) + 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) axisLabel(opt *axisOption) { - style := opt.style - data := *opt.data - if style.FontColor.IsZero() || len(data) == 0 { - return - } - r := d.Render - - s := style.Style() - s.GetTextOptions().WriteTextOptionsToRenderer(r) - - width := d.Box.Width() - height := d.Box.Height() - textList := data.TextList() - count := len(textList) - x0 := 0 - y0 := 0 - tickLength := style.GetTickLength() - - // 坐标轴文本 - switch style.Position { - case PositionLeft: - values := autoDivide(height, count) - textList := data.TextList() - // 由下往上 - reverseIntSlice(values) - for index, text := range textList { - y := values[index] - height := y - values[index+1] - b := r.MeasureText(text) - y -= (height - b.Height()) >> 1 - x := x0 + opt.textMaxWith - (b.Width()) - d.text(text, x, y) - } - case PositionRight: - values := autoDivide(height, count) - textList := data.TextList() - // 由下往上 - reverseIntSlice(values) - for index, text := range textList { - y := values[index] - height := y - values[index+1] - b := r.MeasureText(text) - y -= (height - b.Height()) >> 1 - x := width - opt.textMaxWith - d.text(text, x, y) - } - case PositionTop: - y0 = tickLength + style.Offset - values := autoDivide(width, count) - maxIndex := len(values) - 2 - for index, text := range data.TextList() { - if index > maxIndex { - break - } - x := values[index] - width := values[index+1] - x - b := r.MeasureText(text) - leftOffset := (width - b.Width()) >> 1 - d.text(text, x+leftOffset, y0) - } - default: - // 定位bottom,重新计算y0的定位 - y0 = height - tickLength + style.Offset - values := autoDivide(width, count) - maxIndex := len(values) - 2 - for index, text := range data.TextList() { - if index > maxIndex { - break - } - x := values[index] - width := values[index+1] - x - b := r.MeasureText(text) - leftOffset := (width - b.Width()) >> 1 - d.text(text, x+leftOffset, y0) - } - } -} - -func (d *draw) axisLine(opt *axisOption) { - - r := d.Render - style := opt.style - s := style.Style() - s.GetStrokeOptions().WriteDrawingOptionsToRenderer(r) - - x0 := 0 - y0 := 0 - x1 := 0 - y1 := 0 - width := d.Box.Width() - height := d.Box.Height() - labelMargin := style.GetLabelMargin() - - // 轴线 - labelHeight := labelMargin + opt.textMaxHeight - labelWidth := labelMargin + opt.textMaxWith - tickLength := style.GetTickLength() - switch style.Position { - case PositionLeft: - x0 = tickLength + style.Offset + labelWidth - x1 = x0 - y0 = 0 - y1 = height - case PositionRight: - x0 = width + style.Offset - labelWidth - x1 = x0 - y0 = 0 - y1 = height - case PositionTop: - x0 = 0 - x1 = width - y0 = style.Offset + labelHeight - y1 = y0 - // bottom - default: - x0 = 0 - x1 = width - y0 = height - tickLength + style.Offset - labelHeight - y1 = y0 - } - - d.moveTo(x0, y0) - d.lineTo(x1, y1) - r.FillStroke() -} - -func (d *draw) axisTick(opt *axisOption) { - r := d.Render - - style := opt.style - s := style.Style() - s.GetStrokeOptions().WriteDrawingOptionsToRenderer(r) - - x0 := 0 - y0 := 0 - width := d.Box.Width() - height := d.Box.Height() - data := *opt.data - tickCount := len(data) - labelMargin := style.GetLabelMargin() - - tickLengthValue := style.GetTickLength() - labelHeight := labelMargin + opt.textMaxHeight - labelWidth := labelMargin + opt.textMaxWith - switch style.Position { - case PositionLeft: - x0 += labelWidth - values := autoDivide(height, tickCount) - for _, v := range values { - x := x0 - y := v - d.moveTo(x, y) - d.lineTo(x+tickLengthValue, y) - r.Stroke() - } - case PositionRight: - values := autoDivide(height, tickCount) - x0 = width - labelWidth - for _, v := range values { - x := x0 - y := v - d.moveTo(x, y) - d.lineTo(x+tickLengthValue, y) - r.Stroke() - } - case PositionTop: - values := autoDivide(width, tickCount) - y0 = style.Offset + labelHeight - for _, v := range values { - x := v - y := y0 - d.moveTo(x, y-tickLengthValue) - d.lineTo(x, y) - r.Stroke() - } - - default: - values := autoDivide(width, tickCount) - y0 = height + style.Offset - labelHeight - for _, v := range values { - x := v - y := y0 - d.moveTo(x, y-tickLengthValue) - d.lineTo(x, y) - r.Stroke() - } - } -} - -func (d *draw) axisMeasureTextMaxWidthHeight(data AxisDataList, style AxisStyle) (int, int) { - r := d.Render - s := style.Style() - s.GetStrokeOptions().WriteDrawingOptionsToRenderer(r) - s.GetTextOptions().WriteTextOptionsToRenderer(r) - return measureTextMaxWidthHeight(data.TextList(), r) -} - -func (d *draw) Axis(data AxisDataList, style AxisStyle) { - if style.Show != nil && !*style.Show { - return - } - r := d.Render - s := style.Style() - s.GetTextOptions().WriteTextOptionsToRenderer(r) - textMaxWidth, textMaxHeight := d.axisMeasureTextMaxWidthHeight(data, style) - opt := &axisOption{ - data: &data, - style: &style, - textMaxWith: textMaxWidth, - textMaxHeight: textMaxHeight, - } - - // 坐标轴线 - d.axisLine(opt) - d.axisTick(opt) - // 坐标文本 - d.axisLabel(opt) -} diff --git a/draw_test.go b/draw_test.go new file mode 100644 index 0000000..ef449dd --- /dev/null +++ b/draw_test.go @@ -0,0 +1,242 @@ +// 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 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 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: "", + }, + // circle + { + fn: func(d *Draw) { + d.circle(5, 2, 3) + }, + result: "", + }, + // text + { + fn: func(d *Draw) { + d.text("hello world!", 3, 6) + }, + result: "", + }, + // line stroke + { + fn: func(d *Draw) { + d.lineStroke([]Point{ + { + X: 1, + Y: 2, + }, + { + X: 3, + Y: 4, + }, + }, LineStyle{ + StrokeColor: drawing.ColorBlack, + StrokeWidth: 1, + }) + }, + result: "", + }, + // set background + { + fn: func(d *Draw) { + d.setBackground(400, 300, chart.ColorWhite) + }, + result: "", + }, + } + 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)) + } +} diff --git a/echarts.go b/echarts.go deleted file mode 100644 index 7e1884c..0000000 --- a/echarts.go +++ /dev/null @@ -1,403 +0,0 @@ -// MIT License - -// Copyright (c) 2021 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" - "strings" - - "github.com/wcharczuk/go-chart/v2" -) - -type EChartStyle struct { - Color string `json:"color"` -} -type ECharsSeriesData struct { - Value float64 `json:"value"` - Name string `json:"name"` - ItemStyle EChartStyle `json:"itemStyle"` -} -type _ECharsSeriesData ECharsSeriesData - -func convertToArray(data []byte) []byte { - data = bytes.TrimSpace(data) - if len(data) == 0 { - return nil - } - if data[0] != '[' { - data = []byte("[" + string(data) + "]") - } - return data -} - -func (es *ECharsSeriesData) UnmarshalJSON(data []byte) error { - data = bytes.TrimSpace(data) - if len(data) == 0 { - return nil - } - if regexp.MustCompile(`^\d+`).Match(data) { - v, err := strconv.ParseFloat(string(data), 64) - if err != nil { - return err - } - es.Value = v - return nil - } - v := _ECharsSeriesData{} - 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 EChartsPadding struct { - box chart.Box -} - -type Position string - -func (lp *Position) 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)(lp) - return json.Unmarshal(data, s) -} - -func (ep *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: - ep.box = chart.Box{ - Left: arr[0], - Top: arr[0], - Bottom: arr[0], - Right: arr[0], - } - case 2: - ep.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] - } - // 上右下左 - ep.box = chart.Box{ - Top: result[0], - Right: result[1], - Bottom: result[2], - Left: result[3], - } - } - return nil -} - -type EChartsYAxis struct { - Data []struct { - Min *float64 `json:"min"` - Max *float64 `json:"max"` - // Interval int `json:"interval"` - AxisLabel struct { - Formatter string `json:"formatter"` - } `json:"axisLabel"` - } `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 EChartsXAxis struct { - Data []struct { - // Type string `json:"type"` - BoundaryGap *bool `json:"boundaryGap"` - SplitNumber int `json:"splitNumber"` - Data []string `json:"data"` - } -} - -func (ex *EChartsXAxis) UnmarshalJSON(data []byte) error { - data = convertToArray(data) - if len(data) == 0 { - return nil - } - return json.Unmarshal(data, &ex.Data) -} - -type EChartsLabelOption struct { - Show bool `json:"show"` - Distance int `json:"distance"` -} - -func (elo EChartsLabelOption) ToLabel() SeriesLabel { - if !elo.Show { - return SeriesLabel{} - } - return SeriesLabel{ - Show: true, - Offset: chart.Box{ - // 默认位置为top,因此设置为负 - Top: -elo.Distance, - }, - } -} - -type ECharsOptions struct { - Theme string `json:"theme"` - Padding EChartsPadding `json:"padding"` - Title struct { - Text string `json:"text"` - Left Position `json:"left"` - Top Position `json:"top"` - TextStyle struct { - Color string `json:"color"` - FontFamily string `json:"fontFamily"` - FontSize float64 `json:"fontSize"` - Height float64 `json:"height"` - } `json:"textStyle"` - } `json:"title"` - XAxis EChartsXAxis `json:"xAxis"` - YAxis EChartsYAxis `json:"yAxis"` - Legend struct { - Data []string `json:"data"` - Align string `json:"align"` - Padding EChartsPadding `json:"padding"` - Left Position `json:"left"` - Right Position `json:"right"` - // Top string `json:"top"` - // Bottom string `json:"bottom"` - } `json:"legend"` - Series []struct { - Data []ECharsSeriesData `json:"data"` - Type string `json:"type"` - YAxisIndex int `json:"yAxisIndex"` - ItemStyle EChartStyle `json:"itemStyle"` - // label的配置 - Label EChartsLabelOption `json:"label"` - } `json:"series"` -} - -func convertEChartsSeries(e *ECharsOptions) ([]Series, chart.TickPosition) { - tickPosition := chart.TickPositionUnset - - if len(e.Series) == 0 { - return nil, tickPosition - } - seriesType := e.Series[0].Type - if seriesType == SeriesPie { - series := make([]Series, len(e.Series[0].Data)) - label := e.Series[0].Label.ToLabel() - for index, item := range e.Series[0].Data { - style := chart.Style{} - if item.ItemStyle.Color != "" { - c := parseColor(item.ItemStyle.Color) - style.FillColor = c - style.StrokeColor = c - } - - series[index] = Series{ - Style: style, - Data: []SeriesData{ - { - Value: item.Value, - }, - }, - Type: seriesType, - Name: item.Name, - Label: label, - } - } - return series, tickPosition - } - series := make([]Series, len(e.Series)) - for index, item := range e.Series { - // bar默认tick居中 - if item.Type == SeriesBar { - tickPosition = chart.TickPositionBetweenTicks - } - style := chart.Style{} - if item.ItemStyle.Color != "" { - c := parseColor(item.ItemStyle.Color) - style.FillColor = c - style.StrokeColor = c - } - data := make([]SeriesData, len(item.Data)) - for j, itemData := range item.Data { - sd := SeriesData{ - Value: itemData.Value, - } - if itemData.ItemStyle.Color != "" { - c := parseColor(itemData.ItemStyle.Color) - sd.Style.FillColor = c - sd.Style.StrokeColor = c - } - data[j] = sd - } - series[index] = Series{ - Style: style, - YAxisIndex: item.YAxisIndex, - Data: data, - Type: item.Type, - Label: item.Label.ToLabel(), - } - } - return series, tickPosition -} - -func (e *ECharsOptions) ToOptions() Options { - o := Options{ - Theme: e.Theme, - Padding: e.Padding.box, - } - - titleTextStyle := e.Title.TextStyle - o.Title = Title{ - Text: e.Title.Text, - Left: string(e.Title.Left), - Top: string(e.Title.Top), - Style: chart.Style{ - FontColor: parseColor(titleTextStyle.Color), - FontSize: titleTextStyle.FontSize, - }, - } - if e.Title.TextStyle.FontFamily != "" { - // 如果获取字体失败忽略 - o.Font, _ = GetFont(e.Title.TextStyle.FontFamily) - } - - if titleTextStyle.FontSize != 0 && titleTextStyle.Height > titleTextStyle.FontSize { - padding := int(titleTextStyle.Height-titleTextStyle.FontSize) / 2 - o.Title.Style.Padding.Top = padding - o.Title.Style.Padding.Bottom = padding - } - - boundaryGap := false - if len(e.XAxis.Data) != 0 { - xAxis := e.XAxis.Data[0] - o.XAxis = XAxis{ - Data: xAxis.Data, - SplitNumber: xAxis.SplitNumber, - } - if xAxis.BoundaryGap == nil || *xAxis.BoundaryGap { - boundaryGap = true - } - } - - o.Legend = Legend{ - Data: e.Legend.Data, - Align: e.Legend.Align, - Padding: e.Legend.Padding.box, - Left: string(e.Legend.Left), - Right: string(e.Legend.Right), - } - if len(e.YAxis.Data) != 0 { - yAxisOptions := make([]*YAxisOption, len(e.YAxis.Data)) - for index, item := range e.YAxis.Data { - opt := &YAxisOption{ - Max: item.Max, - Min: item.Min, - } - template := item.AxisLabel.Formatter - if template != "" { - opt.Formater = func(v interface{}) string { - str := defaultFloatFormater(v) - return strings.ReplaceAll(template, "{value}", str) - } - } - yAxisOptions[index] = opt - } - o.YAxisOptions = yAxisOptions - } - - series, tickPosition := convertEChartsSeries(e) - - o.Series = series - - if boundaryGap { - tickPosition = chart.TickPositionBetweenTicks - } - o.TickPosition = tickPosition - return o -} - -func ParseECharsOptions(options string) (Options, error) { - e := ECharsOptions{} - err := json.Unmarshal([]byte(options), &e) - if err != nil { - return Options{}, err - } - - return e.ToOptions(), nil -} - -func echartsRender(options string, rp chart.RendererProvider) ([]byte, error) { - o, err := ParseECharsOptions(options) - if err != nil { - return nil, err - } - g, err := New(o) - if err != nil { - return nil, err - } - return render(g, rp) -} - -func RenderEChartsToPNG(options string) ([]byte, error) { - return echartsRender(options, chart.PNG) -} - -func RenderEChartsToSVG(options string) ([]byte, error) { - return echartsRender(options, chart.SVG) -} diff --git a/echarts_test.go b/echarts_test.go deleted file mode 100644 index 6dcc4d0..0000000 --- a/echarts_test.go +++ /dev/null @@ -1,455 +0,0 @@ -// MIT License - -// Copyright (c) 2021 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 TestConvertToArray(t *testing.T) { - assert := assert.New(t) - - assert.Nil(convertToArray([]byte(" "))) - - assert.Equal([]byte("[{}]"), convertToArray([]byte("{}"))) - assert.Equal([]byte("[{}]"), convertToArray([]byte("[{}]"))) -} - -func TestECharsSeriesData(t *testing.T) { - assert := assert.New(t) - - es := ECharsSeriesData{} - err := es.UnmarshalJSON([]byte(" ")) - assert.Nil(err) - assert.Equal(ECharsSeriesData{}, es) - - es = ECharsSeriesData{} - err = es.UnmarshalJSON([]byte("12.1")) - assert.Nil(err) - assert.Equal(ECharsSeriesData{ - Value: 12.1, - }, es) - - es = ECharsSeriesData{} - err = es.UnmarshalJSON([]byte(`{ - "value": 12.1, - "name": "test", - "itemStyle": { - "color": "#333" - } - }`)) - assert.Nil(err) - assert.Equal(ECharsSeriesData{ - Value: 12.1, - Name: "test", - ItemStyle: EChartStyle{ - Color: "#333", - }, - }, es) -} - -func TestEChartsPadding(t *testing.T) { - assert := assert.New(t) - - ep := EChartsPadding{} - err := ep.UnmarshalJSON([]byte(" ")) - assert.Nil(err) - assert.Equal(EChartsPadding{}, ep) - - ep = EChartsPadding{} - err = ep.UnmarshalJSON([]byte("1")) - assert.Nil(err) - assert.Equal(EChartsPadding{ - box: chart.Box{ - Top: 1, - Left: 1, - Right: 1, - Bottom: 1, - }, - }, ep) - - ep = EChartsPadding{} - err = ep.UnmarshalJSON([]byte("[1, 2]")) - assert.Nil(err) - assert.Equal(EChartsPadding{ - box: chart.Box{ - Top: 1, - Left: 2, - Right: 2, - Bottom: 1, - }, - }, ep) - - ep = EChartsPadding{} - err = ep.UnmarshalJSON([]byte("[1, 2, 3]")) - assert.Nil(err) - assert.Equal(EChartsPadding{ - box: chart.Box{ - Top: 1, - Right: 2, - Bottom: 3, - Left: 2, - }, - }, ep) - - ep = EChartsPadding{} - err = ep.UnmarshalJSON([]byte("[1, 2, 3, 4]")) - assert.Nil(err) - assert.Equal(EChartsPadding{ - box: chart.Box{ - Top: 1, - Right: 2, - Bottom: 3, - Left: 4, - }, - }, ep) -} - -func TestConvertEChartsSeries(t *testing.T) { - assert := assert.New(t) - - seriesList, tickPosition := convertEChartsSeries(&ECharsOptions{}) - assert.Empty(seriesList) - assert.Equal(chart.TickPositionUnset, tickPosition) - - e := ECharsOptions{} - err := json.Unmarshal([]byte(`{ - "title": { - "text": "Referer of a Website" - }, - "series": [ - { - "name": "Access From", - "type": "pie", - "radius": "50%", - "data": [ - { - "value": 1048, - "name": "Search Engine" - }, - { - "value": 735, - "name": "Direct" - } - ] - } - ] - }`), &e) - assert.Nil(err) - seriesList, tickPosition = convertEChartsSeries(&e) - assert.Equal(chart.TickPositionUnset, tickPosition) - assert.Equal([]Series{ - { - Data: []SeriesData{ - { - Value: 1048, - }, - }, - Type: SeriesPie, - Name: "Search Engine", - }, - { - Data: []SeriesData{ - { - Value: 735, - }, - }, - Type: SeriesPie, - Name: "Direct", - }, - }, seriesList) - - err = json.Unmarshal([]byte(`{ - "series": [ - { - "name": "Evaporation", - "type": "bar", - "data": [2, { - "value": 4.9, - "itemStyle": { - "color": "#a90000" - } - }, 7, 23.2, 25.6, 76.7, 135.6] - }, - { - "name": "Precipitation", - "type": "bar", - "data": [2.6, 5.9, 9, 26.4, 28.7, 70.7, 175.6] - }, - { - "name": "Temperature", - "type": "line", - "yAxisIndex": 1, - "data": [2, 2.2, 3.3, 4.5, 6.3, 10.2, 20.3] - } - ] - }`), &e) - assert.Nil(err) - bar1Data := NewSeriesDataListFromFloat([]float64{ - 2, 4.9, 7, 23.2, 25.6, 76.7, 135.6, - }) - bar1Data[1].Style.FillColor = parseColor("#a90000") - bar1Data[1].Style.StrokeColor = bar1Data[1].Style.FillColor - - seriesList, tickPosition = convertEChartsSeries(&e) - assert.Equal(chart.TickPositionBetweenTicks, tickPosition) - assert.Equal([]Series{ - { - Data: bar1Data, - Type: SeriesBar, - }, - { - Data: NewSeriesDataListFromFloat([]float64{ - 2.6, 5.9, 9, 26.4, 28.7, 70.7, 175.6, - }), - Type: SeriesBar, - }, - { - Data: NewSeriesDataListFromFloat([]float64{ - 2, 2.2, 3.3, 4.5, 6.3, 10.2, 20.3, - }), - Type: SeriesLine, - YAxisIndex: 1, - }, - }, seriesList) - -} - -func TestParseECharsOptions(t *testing.T) { - - assert := assert.New(t) - options, err := ParseECharsOptions(`{ - "theme": "dark", - "padding": [5, 10], - "title": { - "text": "Multi Line", - "textAlign": "left", - "textStyle": { - "color": "#333", - "fontSize": 24, - "height": 40 - } - }, - "legend": { - "align": "left", - "padding": [5, 0, 0, 50], - "data": ["Email", "Union Ads", "Video Ads", "Direct", "Search Engine"] - }, - "xAxis": { - "type": "category", - "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"], - "splitNumber": 10 - }, - "yAxis": [ - { - "min": 0, - "max": 250 - }, - { - "min": 0, - "max": 25 - } - ], - "series": [ - { - "name": "Evaporation", - "type": "bar", - "data": [2, { - "value": 4.9, - "itemStyle": { - "color": "#a90000" - } - }, 7, 23.2, 25.6, 76.7, 135.6] - }, - { - "name": "Precipitation", - "type": "bar", - "itemStyle": { - "color": "#0052d9" - }, - "data": [2.6, 5.9, 9, 26.4, 28.7, 70.7, 175.6] - }, - { - "name": "Temperature", - "type": "line", - "yAxisIndex": 1, - "data": [2, 2.2, 3.3, 4.5, 6.3, 10.2, 20.3] - } - ] - }`) - - assert.Nil(err) - - min1 := float64(0) - max1 := float64(250) - min2 := float64(0) - max2 := float64(25) - - bar1Data := NewSeriesDataListFromFloat([]float64{ - 2, 4.9, 7, 23.2, 25.6, 76.7, 135.6, - }) - bar1Data[1].Style.FillColor = parseColor("#a90000") - bar1Data[1].Style.StrokeColor = bar1Data[1].Style.FillColor - - assert.Equal(Options{ - Theme: ThemeDark, - Padding: chart.Box{ - Top: 5, - Bottom: 5, - Left: 10, - Right: 10, - }, - Title: Title{ - Text: "Multi Line", - Style: chart.Style{ - FontColor: parseColor("#333"), - FontSize: 24, - Padding: chart.Box{ - Top: 8, - Bottom: 8, - }, - }, - }, - Legend: Legend{ - Data: []string{ - "Email", "Union Ads", "Video Ads", "Direct", "Search Engine", - }, - Align: "left", - Padding: chart.Box{ - Top: 5, - Right: 0, - Bottom: 0, - Left: 50, - }, - }, - XAxis: XAxis{ - Data: []string{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}, - SplitNumber: 10, - }, - TickPosition: chart.TickPositionBetweenTicks, - YAxisOptions: []*YAxisOption{ - { - Min: &min1, - Max: &max1, - }, - { - Min: &min2, - Max: &max2, - }, - }, - Series: []Series{ - { - Data: bar1Data, - Type: SeriesBar, - }, - { - Data: NewSeriesDataListFromFloat([]float64{ - 2.6, 5.9, 9, 26.4, 28.7, 70.7, 175.6, - }), - Type: SeriesBar, - Style: chart.Style{ - StrokeColor: drawing.Color{ - R: 0, - G: 82, - B: 217, - A: 255, - }, - FillColor: drawing.Color{ - R: 0, - G: 82, - B: 217, - A: 255, - }, - }, - }, - { - Data: NewSeriesDataListFromFloat([]float64{ - 2, 2.2, 3.3, 4.5, 6.3, 10.2, 20.3, - }), - Type: SeriesLine, - YAxisIndex: 1, - }, - }, - }, options) -} - -func TestUnmarshalJSON(t *testing.T) { - assert := assert.New(t) - var lp Position - err := lp.UnmarshalJSON([]byte("123")) - assert.Nil(err) - assert.Equal("123", string(lp)) - - err = lp.UnmarshalJSON([]byte(`"234"`)) - assert.Nil(err) - assert.Equal("234", string(lp)) -} - -func BenchmarkEChartsRenderPNG(b *testing.B) { - for i := 0; i < b.N; i++ { - _, err := RenderEChartsToPNG(`{ - "title": { - "text": "Line" - }, - "xAxis": { - "type": "category", - "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] - }, - "series": [ - { - "data": [150, 230, 224, 218, 135, 147, 260] - } - ] - }`) - if err != nil { - panic(err) - } - } -} - -func BenchmarkEChartsRenderSVG(b *testing.B) { - for i := 0; i < b.N; i++ { - _, err := RenderEChartsToSVG(`{ - "title": { - "text": "Line" - }, - "xAxis": { - "type": "category", - "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] - }, - "series": [ - { - "data": [150, 230, 224, 218, 135, 147, 260] - } - ] - }`) - if err != nil { - panic(err) - } - } -} diff --git a/examples/basic/main.go b/examples/basic/main.go deleted file mode 100644 index 9efc745..0000000 --- a/examples/basic/main.go +++ /dev/null @@ -1,33 +0,0 @@ -package main - -import ( - "os" - - charts "github.com/vicanso/go-charts" -) - -func main() { - buf, err := charts.RenderEChartsToPNG(`{ - "title": { - "text": "Line" - }, - "xAxis": { - "type": "category", - "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] - }, - "series": [ - { - "data": [150, 230, 224, 218, 135, 147, 260] - } - ] - }`) - if err != nil { - panic(err) - } - file, err := os.Create("output.png") - if err != nil { - panic(err) - } - defer file.Close() - file.Write(buf) -} diff --git a/examples/charts/main.go b/examples/charts/main.go deleted file mode 100644 index 1828a52..0000000 --- a/examples/charts/main.go +++ /dev/null @@ -1,394 +0,0 @@ -package main - -import ( - "bytes" - "net/http" - - charts "github.com/vicanso/go-charts" -) - -var html = ` - -
- - - - - -{{option}}
- {{svg}}
-