diff --git a/chart.go b/chart.go index a485df5..7c511de 100644 --- a/chart.go +++ b/chart.go @@ -43,6 +43,7 @@ type Point struct { } type ChartOption struct { + Type string Font *truetype.Font Theme string Title TitleOption @@ -271,6 +272,7 @@ func Render(opt ChartOption) (*Draw, error) { func chartBasicRender(opt *ChartOption) (*basicRenderResult, error) { d, err := NewDraw( DrawOption{ + Type: opt.Type, Parent: opt.Parent, Width: opt.getWidth(), Height: opt.getHeight(), diff --git a/draw.go b/draw.go index bdd5a2b..c7f0849 100644 --- a/draw.go +++ b/draw.go @@ -25,6 +25,7 @@ package charts import ( "bytes" "errors" + "math" "github.com/golang/freetype/truetype" "github.com/wcharczuk/go-chart/v2" @@ -151,6 +152,30 @@ func (d *Draw) lineTo(x, y int) { d.Render.LineTo(x+d.Box.Left, y+d.Box.Top) } +func (d *Draw) pin(x, y, width int) { + r := float64(width) / 2 + y -= width / 4 + angle := chart.DegreesToRadians(15) + + startAngle := math.Pi/2 + angle + delta := 2*math.Pi - 2*angle + d.arcTo(x, y, r, r, startAngle, delta) + d.lineTo(x, y) + d.Render.Fill() + startX := x - int(r) + startY := y + endX := x + int(r) + endY := y + d.moveTo(startX, startY) + + left := d.Box.Left + top := d.Box.Top + cx := x + cy := y + int(r*2.5) + d.Render.QuadCurveTo(cx+left, cy+top, endX+left, endY+top) + d.Render.Fill() +} + func (d *Draw) circle(radius float64, x, y int) { d.Render.Circle(radius, x+d.Box.Left, y+d.Box.Top) } diff --git a/draw_test.go b/draw_test.go index 9627d0e..dcba961 100644 --- a/draw_test.go +++ b/draw_test.go @@ -239,6 +239,27 @@ func TestDraw(t *testing.T) { }, result: "\\n", }, + { + fn: func(d *Draw) { + chart.Style{ + StrokeWidth: 1, + StrokeColor: drawing.Color{ + R: 84, + G: 112, + B: 198, + A: 255, + }, + FillColor: drawing.Color{ + R: 84, + G: 112, + B: 198, + A: 255, + }, + }.WriteToRenderer(d.Render) + d.pin(30, 30, 30) + }, + result: "\\n", + }, } for _, tt := range tests { d, err := NewDraw(DrawOption{ diff --git a/legend.go b/legend.go index e1202cb..7b6722e 100644 --- a/legend.go +++ b/legend.go @@ -129,9 +129,14 @@ func (l *legend) Render() (chart.Box, error) { } x = left for index, text := range opt.Data { + seriesColor := theme.GetSeriesColor(index) + fillColor := seriesColor + if !theme.IsDark() { + fillColor = theme.GetBackgroundColor() + } style := chart.Style{ - StrokeColor: theme.GetSeriesColor(index), - FillColor: theme.GetSeriesColor(index), + StrokeColor: seriesColor, + FillColor: fillColor, StrokeWidth: 3, } style.GetFillAndStrokeOptions().WriteDrawingOptionsToRenderer(r) @@ -144,7 +149,7 @@ func (l *legend) Render() (chart.Box, error) { x = left renderText = func() { x += textPadding - legendDraw.text(text, x, y+legendDotHeight-2) + legendDraw.text(text, x, y+legendDotHeight) x += textBox.Width() y += (2*legendDotHeight + legendMargin) } @@ -156,7 +161,7 @@ func (l *legend) Render() (chart.Box, error) { } renderText = func() { x += textPadding - legendDraw.text(text, x, y+legendDotHeight-2) + legendDraw.text(text, x, y+legendDotHeight) x += textBox.Width() x += textPadding } diff --git a/legend_test.go b/legend_test.go index 0274269..db9d459 100644 --- a/legend_test.go +++ b/legend_test.go @@ -65,7 +65,7 @@ func TestLegendRender(t *testing.T) { Style: style, }) }, - result: "\\nMonTueWed", + result: "\\nMonTueWed", box: chart.Box{ Right: 214, Bottom: 25, @@ -86,7 +86,7 @@ func TestLegendRender(t *testing.T) { Style: style, }) }, - result: "\\nMonTueWed", + result: "\\nMonTueWed", box: chart.Box{ Right: 400, Bottom: 25, @@ -106,7 +106,7 @@ func TestLegendRender(t *testing.T) { Style: style, }) }, - result: "\\nMonTueWed", + result: "\\nMonTueWed", box: chart.Box{ Right: 307, Bottom: 25, @@ -127,7 +127,7 @@ func TestLegendRender(t *testing.T) { Orient: OrientVertical, }) }, - result: "\\nMonTueWed", + result: "\\nMonTueWed", box: chart.Box{ Right: 61, Bottom: 70, @@ -152,7 +152,7 @@ func TestLegendRender(t *testing.T) { Right: 101, Bottom: 70, }, - result: "\\nMonTueWed", + result: "\\nMonTueWed", }, } diff --git a/line_chart.go b/line_chart.go index c9b1b7a..91e1a6d 100644 --- a/line_chart.go +++ b/line_chart.go @@ -23,6 +23,8 @@ package charts import ( + "math" + "github.com/wcharczuk/go-chart/v2" "github.com/wcharczuk/go-chart/v2/drawing" ) @@ -47,7 +49,19 @@ func lineChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error) xRange := result.xRange for i, series := range opt.SeriesList { points := make([]Point, 0) + minIndex := -1 + maxIndex := -1 + minValue := math.MaxFloat64 + maxValue := -math.MaxFloat64 for j, item := range series.Data { + if item.Value < minValue { + minIndex = j + minValue = item.Value + } + if item.Value > maxValue { + maxIndex = j + maxValue = item.Value + } y := yRange.getRestHeight(item.Value) x := xRange.getWidth(float64(j)) points = append(points, Point{ @@ -86,6 +100,34 @@ func lineChartRender(opt ChartOption, result *basicRenderResult) (*Draw, error) DotWidth: 2, DotFillColor: dotFillColor, }) + // draw mark point + symbolSize := 30 + if series.MarkPoint.SymbolSize > 0 { + symbolSize = series.MarkPoint.SymbolSize + } + for _, markPointData := range series.MarkPoint.Data { + p := points[minIndex] + value := minValue + switch markPointData.Type { + case SeriesMarkPointDataTypeMax: + p = points[maxIndex] + value = maxValue + } + chart.Style{ + FillColor: seriesColor, + }.WriteToRenderer(r) + d.pin(p.X, p.Y-symbolSize>>1, symbolSize) + + chart.Style{ + FontColor: NewTheme(ThemeDark).GetTextColor(), + FontSize: 10, + StrokeWidth: 1, + Font: opt.Font, + }.WriteTextOptionsToRenderer(d.Render) + text := commafWithDigits(value) + textBox := r.MeasureText(text) + d.text(text, p.X-textBox.Width()>>1, p.Y-symbolSize>>1-2) + } } return result.d, nil diff --git a/range.go b/range.go index 8a3db1e..0d1b459 100644 --- a/range.go +++ b/range.go @@ -24,8 +24,6 @@ package charts import ( "math" - - "github.com/dustin/go-humanize" ) type Range struct { @@ -69,7 +67,7 @@ func (r Range) Values() []string { values := make([]string, 0) for i := 0; i <= r.divideCount; i++ { v := r.Min + float64(i)*offset - value := humanize.CommafWithDigits(v, 2) + value := commafWithDigits(v) values = append(values, value) } return values diff --git a/series.go b/series.go index 1ab95f8..8b5161d 100644 --- a/series.go +++ b/series.go @@ -60,6 +60,19 @@ type SeriesLabel struct { Color drawing.Color Show bool } + +const ( + SeriesMarkPointDataTypeMax = "max" + SeriesMarkPointDataTypeMin = "min" +) + +type SeriesMarkPointData struct { + Type string +} +type SeriesMarkPoint struct { + SymbolSize int + Data []SeriesMarkPointData +} type Series struct { index int Type string @@ -69,7 +82,8 @@ type Series struct { Label SeriesLabel Name string // Radius of Pie chart, e.g.: 40% - Radius string + Radius string + MarkPoint SeriesMarkPoint } type LabelFormatter func(index int, value float64, percent float64) string diff --git a/theme.go b/theme.go index 5aa4bf9..23a010c 100644 --- a/theme.go +++ b/theme.go @@ -131,8 +131,8 @@ func (t *Theme) GetTextColor() drawing.Color { if t.IsDark() { return drawing.Color{ R: 238, - G: 241, - B: 250, + G: 238, + B: 238, A: 255, } } diff --git a/util.go b/util.go index 9765bb5..5c1415d 100644 --- a/util.go +++ b/util.go @@ -26,6 +26,7 @@ import ( "strconv" "strings" + "github.com/dustin/go-humanize" "github.com/wcharczuk/go-chart/v2" ) @@ -109,3 +110,15 @@ func toFloatPoint(f float64) *float64 { v := f return &v } +func commafWithDigits(value float64) string { + decimals := 2 + m := float64(1000 * 1000) + if value >= m { + return humanize.CommafWithDigits(value/m, decimals) + " M" + } + k := float64(1000) + if value >= k { + return humanize.CommafWithDigits(value/k, decimals) + " K" + } + return humanize.CommafWithDigits(value, decimals) +} diff --git a/yaxis.go b/yaxis.go index 8603924..1742e99 100644 --- a/yaxis.go +++ b/yaxis.go @@ -23,6 +23,8 @@ package charts import ( + "strings" + "github.com/wcharczuk/go-chart/v2" ) @@ -33,6 +35,8 @@ type YAxisOption struct { Max *float64 // Hidden y axis Hidden bool + // Formatter for y axis text value + Formatter string } const YAxisWidth = 40 @@ -40,7 +44,15 @@ const YAxisWidth = 40 func drawYAxis(p *Draw, opt *ChartOption, xAxisHeight int, padding chart.Box) (*Range, error) { theme := NewTheme(opt.Theme) yRange := opt.getYRange(0) - data := NewAxisDataListFromStringList(yRange.Values()) + values := yRange.Values() + formatter := opt.YAxis.Formatter + if len(formatter) != 0 { + for index, text := range values { + values[index] = strings.ReplaceAll(formatter, "{value}", text) + } + } + + data := NewAxisDataListFromStringList(values) style := AxisOption{ Position: PositionLeft, BoundaryGap: FalseFlag(),