// 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" "github.com/golang/freetype/truetype" "github.com/wcharczuk/go-chart/v2" "github.com/wcharczuk/go-chart/v2/drawing" ) const ( PositionLeft = "left" PositionRight = "right" PositionTop = "top" PositionBottom = "bottom" ) type draw struct { Render chart.Renderer Box chart.Box parent *draw } type Point struct { X int Y int } type LineStyle struct { ClassName string StrokeDashArray []float64 StrokeColor drawing.Color StrokeWidth float64 FillColor drawing.Color DotWidth float64 DotColor drawing.Color DotFillColor drawing.Color } 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, } } 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, } } 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, } if s.FontSize == 0 { s.FontSize = chart.DefaultFontSize } if s.Font == nil { s.Font, _ = chart.GetDefaultFont() } 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 } func NewAxisDataListFromStringList(textList []string) AxisDataList { list := make(AxisDataList, len(textList)) for index, text := range textList { list[index] = AxisData{ Text: text, } } return list } 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 { return d.parent } 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 } 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) lineTo(x, y int) { d.Render.LineTo(x+d.Box.Left, y+d.Box.Top) } 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) 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) lineFill(points []Point, style LineStyle) { s := style.Style() if !(s.ShouldDrawStroke() && 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) } } 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) 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) }