diff --git a/bar_chart.go b/bar_chart.go index 597388c..ae5adfb 100644 --- a/bar_chart.go +++ b/bar_chart.go @@ -37,7 +37,6 @@ func barChartRender(opt ChartOption, result *basicRenderResult) ([]*markPointRen if err != nil { return nil, err } - // yRange := result.yRange xRange := result.xRange x0, x1 := xRange.GetRange(0) width := int(x1 - x0) @@ -45,6 +44,10 @@ func barChartRender(opt ChartOption, result *basicRenderResult) ([]*markPointRen margin := 10 // 每一个bar之间的margin barMargin := 5 + if width < 50 { + margin = 5 + barMargin = 3 + } seriesCount := len(opt.SeriesList) // 总的宽度-两个margin-(总数-1)的barMargin @@ -80,9 +83,9 @@ func barChartRender(opt ChartOption, result *basicRenderResult) ([]*markPointRen Series: &series, Range: yRange, }) + divideValues := xRange.AutoDivide() for j, item := range series.Data { - x0, _ := xRange.GetRange(j) - x := int(x0) + x := divideValues[j] x += margin if i != 0 { x += i * (barWidth + barMargin) diff --git a/chart.go b/chart.go index 3321f50..de53cfc 100644 --- a/chart.go +++ b/chart.go @@ -144,17 +144,24 @@ func (o *ChartOption) FillDefault(theme string) { } func (o *ChartOption) getWidth() int { - if o.Width == 0 { - return 600 + if o.Width != 0 { + return o.Width } - return o.Width + if o.Parent != nil { + return o.Parent.Box.Width() + } + return 600 } func (o *ChartOption) getHeight() int { - if o.Height == 0 { - return 400 + + if o.Height != 0 { + return o.Height } - return o.Height + if o.Parent != nil { + return o.Parent.Box.Height() + } + return 400 } func (o *ChartOption) newYRange(axisIndex int) Range { @@ -219,6 +226,7 @@ func Render(opt ChartOption) (*Draw, error) { if len(opt.SeriesList) == 0 { return nil, errors.New("series can not be nil") } + opt.FillDefault(opt.Theme) lineSeries := make([]Series, 0) barSeries := make([]Series, 0) @@ -321,7 +329,6 @@ func Render(opt ChartOption) (*Draw, error) { } func chartBasicRender(opt *ChartOption) (*basicRenderResult, error) { - opt.FillDefault(opt.Theme) d, err := NewDraw( DrawOption{ Type: opt.Type, diff --git a/examples/demo/main.go b/examples/demo/main.go index f0e32c7..978bef6 100644 --- a/examples/demo/main.go +++ b/examples/demo/main.go @@ -6,6 +6,7 @@ import ( charts "github.com/vicanso/go-charts" "github.com/wcharczuk/go-chart/v2" + "github.com/wcharczuk/go-chart/v2/drawing" ) var html = ` @@ -13,7 +14,6 @@ var html = ` - @@ -61,120 +61,733 @@ var html = ` ` -func indexHandler(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/" { +func indexHandler(w http.ResponseWriter, req *http.Request) { + if req.URL.Path != "/" { return } - - d, err := charts.NewLineChart(charts.LineChartOption{ - ChartOption: charts.ChartOption{ - Theme: "dark", + theme := req.URL.Query().Get("theme") + chartOptions := []charts.ChartOption{ + // 普通折线图 + { + Theme: theme, + Title: charts.TitleOption{ + Text: "Line", + }, + Legend: charts.NewLegendOption([]string{ + "Email", + "Union Ads", + "Video Ads", + "Direct", + "Search Engine", + }), + XAxis: charts.NewXAxisOption([]string{ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", + }), + SeriesList: []charts.Series{ + charts.NewSeriesFromValues([]float64{ + 120, + 132, + 101, + 134, + 90, + 230, + 210, + }), + charts.NewSeriesFromValues([]float64{ + 220, + 182, + 191, + 234, + 290, + 330, + 310, + }), + charts.NewSeriesFromValues([]float64{ + 150, + 232, + 201, + 154, + 190, + 330, + 410, + }), + charts.NewSeriesFromValues([]float64{ + 320, + 332, + 301, + 334, + 390, + 330, + 320, + }), + charts.NewSeriesFromValues([]float64{ + 820, + 932, + 901, + 934, + 1290, + 1330, + 1320, + }), + }, + }, + // 温度折线图 + { + Theme: theme, + Title: charts.TitleOption{ + Text: "Temperature Change in the Coming Week", + }, Padding: chart.Box{ - Left: 5, - Top: 15, - Bottom: 5, - Right: 10, - }, - XAxis: charts.XAxisOption{ - Data: []string{ - "Mon", - "Tue", - "Wed", - "Thu", - "Fri", - "Sat", - "Sun", - }, - // BoundaryGap: charts.FalseFlag(), + Top: 20, + Left: 20, + Right: 30, + Bottom: 20, }, + Legend: charts.NewLegendOption([]string{ + "Highest", + "Lowest", + }, charts.PositionRight), + XAxis: charts.NewXAxisOption([]string{ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", + }, charts.FalseFlag()), SeriesList: []charts.Series{ { + Data: charts.NewSeriesDataFromValues([]float64{ + 14, + 11, + 13, + 11, + 12, + 12, + 7, + }), + MarkPoint: charts.NewMarkPoint(charts.SeriesMarkDataTypeMax, charts.SeriesMarkDataTypeMin), + MarkLine: charts.NewMarkLine(charts.SeriesMarkDataTypeAverage), + }, + { + Data: charts.NewSeriesDataFromValues([]float64{ + 1, + -2, + 2, + 5, + 3, + 2, + 0, + }), + MarkLine: charts.NewMarkLine(charts.SeriesMarkDataTypeAverage), + }, + }, + }, + // 柱状图 + { + Theme: theme, + Title: charts.TitleOption{ + Text: "Bar", + }, + XAxis: charts.NewXAxisOption([]string{ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", + }), + Legend: charts.NewLegendOption([]string{ + "Rainfall", + "Evaporation", + }), + SeriesList: []charts.Series{ + charts.NewSeriesFromValues([]float64{ + 120, + 200, + 150, + 80, + 70, + 110, + 130, + }, charts.ChartTypeBar), + { + Type: charts.ChartTypeBar, Data: []charts.SeriesData{ { - Value: 150, + Value: 100, + }, + { + Value: 190, + Style: chart.Style{ + FillColor: drawing.Color{ + R: 169, + G: 0, + B: 0, + A: 255, + }, + }, }, { Value: 230, }, { - Value: 224, + Value: 140, }, { - Value: 218, + Value: 100, }, { - Value: 135, + Value: 200, }, { - Value: 147, - }, - { - Value: 260, - }, - }, - }, - { - Data: []charts.SeriesData{ - { - Value: 220, - }, - { - Value: 182, - }, - { - Value: 191, - }, - { - Value: 234, - }, - { - Value: 290, - }, - { - Value: 330, - }, - { - Value: 310, - }, - }, - }, - { - Data: []charts.SeriesData{ - { - Value: 150, - }, - { - Value: 232, - }, - { - Value: 201, - }, - { - Value: 154, - }, - { - Value: 190, - }, - { - Value: 330, - }, - { - Value: 410, + Value: 180, }, }, }, }, }, - }) + // 柱状图+mark + { + Theme: theme, + Title: charts.TitleOption{ + Text: "Rainfall vs Evaporation", + Subtext: "Fake Data", + }, + Padding: chart.Box{ + Top: 20, + Right: 20, + Bottom: 20, + Left: 20, + }, + XAxis: charts.NewXAxisOption([]string{ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + }), + Legend: charts.NewLegendOption([]string{ + "Rainfall", + "Evaporation", + }, charts.PositionRight), + SeriesList: []charts.Series{ + { + Type: charts.ChartTypeBar, + Data: charts.NewSeriesDataFromValues([]float64{ + 2.0, + 4.9, + 7.0, + 23.2, + 25.6, + 76.7, + 135.6, + 162.2, + 32.6, + 20.0, + 6.4, + 3.3, + }), + MarkPoint: charts.NewMarkPoint( + charts.SeriesMarkDataTypeMax, + charts.SeriesMarkDataTypeMin, + ), + MarkLine: charts.NewMarkLine( + charts.SeriesMarkDataTypeAverage, + ), + }, + { + Type: charts.ChartTypeBar, + Data: charts.NewSeriesDataFromValues([]float64{ + 2.6, + 5.9, + 9.0, + 26.4, + 28.7, + 70.7, + 175.6, + 182.2, + 48.7, + 18.8, + 6.0, + 2.3, + }), + MarkPoint: charts.NewMarkPoint( + charts.SeriesMarkDataTypeMax, + charts.SeriesMarkDataTypeMin, + ), + MarkLine: charts.NewMarkLine( + charts.SeriesMarkDataTypeAverage, + ), + }, + }, + }, + // 双Y轴示例 + { + Theme: theme, + XAxis: charts.NewXAxisOption([]string{ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + }), + Legend: charts.NewLegendOption([]string{ + "Evaporation", + "Precipitation", + "Temperature", + }), + YAxisList: []charts.YAxisOption{ + { + Formatter: "{value}°C", + Color: drawing.Color{ + R: 250, + G: 200, + B: 88, + A: 255, + }, + }, + { + Formatter: "{value}ml", + Color: drawing.Color{ + R: 84, + G: 112, + B: 198, + A: 255, + }, + }, + }, + SeriesList: []charts.Series{ + { + Type: charts.ChartTypeBar, + Data: charts.NewSeriesDataFromValues([]float64{ + 2.0, + 4.9, + 7.0, + 23.2, + 25.6, + 76.7, + 135.6, + 162.2, + 32.6, + 20.0, + 6.4, + 3.3, + }), + YAxisIndex: 1, + }, + { + Type: charts.ChartTypeBar, + Data: charts.NewSeriesDataFromValues([]float64{ + 2.6, + 5.9, + 9.0, + 26.4, + 28.7, + 70.7, + 175.6, + 182.2, + 48.7, + 18.8, + 6.0, + 2.3, + }), + YAxisIndex: 1, + }, + { + Data: charts.NewSeriesDataFromValues([]float64{ + 2.0, + 2.2, + 3.3, + 4.5, + 6.3, + 10.2, + 20.3, + 23.4, + 23.0, + 16.5, + 12.0, + 6.2, + }), + }, + }, + }, + // 饼图 + { + Theme: theme, + Title: charts.TitleOption{ + Text: "Referer of a Website", + Subtext: "Fake Data", + Left: charts.PositionCenter, + }, + Legend: charts.LegendOption{ + Orient: charts.OrientVertical, + Data: []string{ + "Search Engine", + "Direct", + "Email", + "Union Ads", + "Video Ads", + }, + Left: charts.PositionLeft, + }, + SeriesList: charts.NewPieSeriesList([]float64{ + 1048, + 735, + 580, + 484, + 300, + }, charts.PieSeriesOption{ + LabelShow: true, + Radius: "35%", + }), + }, + // 多图展示 + { + Theme: theme, + Legend: charts.LegendOption{ + Top: "-90", + Data: []string{ + "Milk Tea", + "Matcha Latte", + "Cheese Cocoa", + "Walnut Brownie", + }, + }, + Padding: chart.Box{ + Top: 100, + }, + XAxis: charts.NewXAxisOption([]string{ + "2012", + "2013", + "2014", + "2015", + "2016", + "2017", + }), + YAxisList: []charts.YAxisOption{ + { + + Min: charts.NewFloatPoint(0), + Max: charts.NewFloatPoint(90), + }, + }, + SeriesList: []charts.Series{ + charts.NewSeriesFromValues([]float64{ + 56.5, + 82.1, + 88.7, + 70.1, + 53.4, + 85.1, + }), + charts.NewSeriesFromValues([]float64{ + 51.1, + 51.4, + 55.1, + 53.3, + 73.8, + 68.7, + }), + charts.NewSeriesFromValues([]float64{ + 40.1, + 62.2, + 69.5, + 36.4, + 45.2, + 32.5, + }, charts.ChartTypeBar), + charts.NewSeriesFromValues([]float64{ + 25.2, + 37.1, + 41.2, + 18, + 33.9, + 49.1, + }, charts.ChartTypeBar), + }, + Children: []charts.ChartOption{ + { + Legend: charts.LegendOption{ + Show: charts.FalseFlag(), + Data: []string{ + "Milk Tea", + "Matcha Latte", + "Cheese Cocoa", + "Walnut Brownie", + }, + }, + Height: 20, + Padding: chart.Box{ + Left: 250, + Top: -80, + }, + SeriesList: charts.NewPieSeriesList([]float64{ + 435.9, + 354.3, + 285.9, + 204.5, + }, charts.PieSeriesOption{ + LabelShow: true, + Radius: "35%", + }), + }, + }, + }, + } + bytesList := make([][]byte, 0) + for _, opt := range chartOptions { + d, err := charts.Render(opt) + if err != nil { + panic(err) + } + buf, err := d.Bytes() + if err != nil { + panic(err) + } + bytesList = append(bytesList, buf) + } + data := bytes.ReplaceAll([]byte(html), []byte("{{body}}"), bytes.Join(bytesList, []byte(""))) + w.Header().Set("Content-Type", "text/html") + w.Write(data) + +} + +func indexHandlerBak(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + return + } + + zero := float64(0) + outputType := r.URL.Query().Get("type") + chartOption := charts.ChartOption{ + // Theme: "dark", + Type: outputType, + Title: charts.TitleOption{ + Left: charts.PositionCenter, + Style: chart.Style{ + FontColor: chart.ColorAlternateBlue, + }, + SubtextStyle: chart.Style{ + FontColor: chart.ColorRed, + }, + Text: "Stacked Line", + Subtext: "Hello World!", + }, + Padding: chart.Box{ + Left: 5, + Top: 15, + Bottom: 5, + Right: 10, + }, + YAxisList: []charts.YAxisOption{ + { + Min: &zero, + }, + { + Formatter: "{value} °C", + // Max: charts.NewFloatPoint(24), + }, + }, + Legend: charts.LegendOption{ + Data: []string{ + "Email", + "Union Ads", + // "Video Ads", + }, + Left: charts.PositionLeft, + // Orient: charts.OrientVertical, + }, + XAxis: charts.XAxisOption{ + Data: []string{ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun", + }, + // SplitNumber: 4, + // BoundaryGap: charts.FalseFlag(), + }, + SeriesList: []charts.Series{ + { + // Type: charts.ChartTypeBar, + MarkPoint: charts.SeriesMarkPoint{ + Data: []charts.SeriesMarkPointData{ + { + Type: "max", + }, + { + Type: "min", + }, + }, + }, + MarkLine: charts.SeriesMarkLine{ + Data: []charts.SeriesMarkLineData{ + // { + // Type: "max", + // }, + { + Type: "average", + }, + }, + }, + Data: charts.NewSeriesDataFromValues([]float64{ + 2.0, + 4.9, + 7.0, + 23.2, + 25.6, + 76.7, + 135.6, + }), + }, + // { + // // Type: charts.ChartTypeBar, + // Data: charts.NewSeriesDataFromValues([]float64{ + // 2.6, + // 5.9, + // 9.0, + // 26.4, + // 28.7, + // 70.7, + // 175.6, + // }), + // }, + { + Data: charts.NewSeriesDataFromValues([]float64{ + 2.0, + 2.2, + 3.3, + 4.5, + 6.3, + 10.2, + 20.3, + }), + YAxisIndex: 1, + }, + // { + // Data: []charts.SeriesData{ + // { + // Value: 220, + // }, + // { + // Value: 182, + // }, + // { + // Value: 191, + // }, + // { + // Value: 234, + // }, + // { + // Value: 290, + // }, + // { + // Value: 330, + // }, + // { + // Value: 310, + // }, + // }, + // }, + // { + // Data: []charts.SeriesData{ + // { + // Value: 150, + // }, + // { + // Value: 232, + // }, + // { + // Value: 201, + // }, + // { + // Value: 154, + // }, + // { + // Value: 190, + // }, + // { + // Value: 330, + // }, + // { + // Value: 410, + // }, + // }, + // }, + }, + // Children: []charts.ChartOption{ + // { + // Padding: chart.Box{ + // Left: 350, + // Top: 0, + // }, + // Legend: charts.LegendOption{ + // Show: charts.FalseFlag(), + // }, + // Width: 150, + // Height: 150, + // SeriesList: []charts.Series{ + // charts.NewSeriesFromValues([]float64{ + // 1048, + // }, charts.ChartTypePie), + // { + // Data: charts.NewSeriesDataFromValues([]float64{ + // 735, + // }), + // Radius: "50%", + // Name: "test", + // }, + // charts.NewSeriesFromValues([]float64{ + // 580, + // }), + // charts.NewSeriesFromValues([]float64{ + // 484, + // }), + // }, + // }, + // }, + } + d, err := charts.Render(chartOption) if err != nil { panic(err) } buf, _ := d.Bytes() - data := bytes.ReplaceAll([]byte(html), []byte("{{body}}"), buf) - w.Header().Set("Content-Type", "text/html") - w.Write(data) + if outputType == "png" { + w.Header().Set("Content-Type", "image/png") + w.Write(buf) + } else { + data := bytes.ReplaceAll([]byte(html), []byte("{{body}}"), buf) + w.Header().Set("Content-Type", "text/html") + w.Write(data) + } } func main() { diff --git a/legend.go b/legend.go index 263b312..f11b50f 100644 --- a/legend.go +++ b/legend.go @@ -49,6 +49,17 @@ type LegendOption struct { // The layout orientation of legend, it can be horizontal or vertical, default is horizontal. Orient string } + +func NewLegendOption(data []string, position ...string) LegendOption { + opt := LegendOption{ + Data: data, + } + if len(position) != 0 { + opt.Left = position[0] + } + return opt +} + type legend struct { d *Draw opt *LegendOption diff --git a/pie_chart.go b/pie_chart.go index a8deb7c..ff24642 100644 --- a/pie_chart.go +++ b/pie_chart.go @@ -38,7 +38,6 @@ func getPieStyle(theme *Theme, index int) chart.Style { StrokeWidth: 1, FillColor: seriesColor, } - } func pieChartRender(opt ChartOption, result *basicRenderResult) error { @@ -86,7 +85,11 @@ func pieChartRender(opt ChartOption, result *basicRenderResult) error { if radius <= 0 { radius = float64(diameter) * defaultRadiusPercent } - labelRadius := radius + 20 + labelLineWidth := 15 + if radius < 50 { + labelLineWidth = 10 + } + labelRadius := radius + float64(labelLineWidth) seriesNames := opt.Legend.Data @@ -126,7 +129,7 @@ func pieChartRender(opt ChartOption, result *basicRenderResult) error { endy := cy + int(labelRadius*math.Sin(angle)) d.moveTo(startx, starty) d.lineTo(endx, endy) - offset := 30 + offset := labelLineWidth if endx < cx { offset *= -1 } diff --git a/range.go b/range.go index 1e02a51..255a51b 100644 --- a/range.go +++ b/range.go @@ -94,6 +94,9 @@ func (r *Range) GetRange(index int) (float64, float64) { unit := float64(r.Size) / float64(r.divideCount) return unit * float64(index), unit * float64(index+1) } +func (r *Range) AutoDivide() []int { + return autoDivide(r.Size, r.divideCount) +} func (r *Range) getWidth(value float64) int { v := value / (r.Max - r.Min) diff --git a/series.go b/series.go index 57fe12a..b1d72a2 100644 --- a/series.go +++ b/series.go @@ -95,6 +95,35 @@ type Series struct { MarkLine SeriesMarkLine } +type PieSeriesOption struct { + Radius string + LabelShow bool +} + +func NewPieSeriesList(values []float64, opts ...PieSeriesOption) []Series { + result := make([]Series, len(values)) + var opt PieSeriesOption + if len(opts) != 0 { + opt = opts[0] + } + for index, v := range values { + s := Series{ + Type: ChartTypePie, + Data: []SeriesData{ + { + Value: v, + }, + }, + Radius: opt.Radius, + Label: SeriesLabel{ + Show: opt.LabelShow, + }, + } + result[index] = s + } + return result +} + type seriesSummary struct { MaxIndex int MaxValue float64 diff --git a/util.go b/util.go index 3064668..03aad20 100644 --- a/util.go +++ b/util.go @@ -40,6 +40,14 @@ func FalseFlag() *bool { return &f } +func ceilFloatToInt(value float64) int { + i := int(value) + if value == float64(i) { + return i + } + return i + 1 +} + func getDefaultInt(value, defaultValue int) int { if value == 0 { return defaultValue diff --git a/xaxis.go b/xaxis.go index cb7cf33..1dca7bb 100644 --- a/xaxis.go +++ b/xaxis.go @@ -40,6 +40,16 @@ type XAxisOption struct { SplitNumber int } +func NewXAxisOption(data []string, boundaryGap ...*bool) XAxisOption { + opt := XAxisOption{ + Data: data, + } + if len(boundaryGap) != 0 { + opt.BoundaryGap = boundaryGap[0] + } + return opt +} + // drawXAxis draws x axis, and returns the height, range of if. func drawXAxis(p *Draw, opt *XAxisOption, yAxisCount int) (int, *Range, error) { if opt.Hidden { diff --git a/yaxis.go b/yaxis.go index cbce44f..99093ec 100644 --- a/yaxis.go +++ b/yaxis.go @@ -26,6 +26,7 @@ import ( "strings" "github.com/wcharczuk/go-chart/v2" + "github.com/wcharczuk/go-chart/v2/drawing" ) type YAxisOption struct { @@ -37,15 +38,19 @@ type YAxisOption struct { Hidden bool // Formatter for y axis text value Formatter string + // Color for y axis + Color drawing.Color } +// TODO 长度是否可以变化 const YAxisWidth = 40 func drawYAxis(p *Draw, opt *ChartOption, axisIndex, xAxisHeight int, padding chart.Box) (*Range, error) { theme := NewTheme(opt.Theme) yRange := opt.newYRange(axisIndex) values := yRange.Values() - formatter := opt.YAxisList[axisIndex].Formatter + yAxis := opt.YAxisList[axisIndex] + formatter := yAxis.Formatter if len(formatter) != 0 { for index, text := range values { values[index] = strings.ReplaceAll(formatter, "{value}", text) @@ -62,6 +67,10 @@ func drawYAxis(p *Draw, opt *ChartOption, axisIndex, xAxisHeight int, padding ch SplitLineColor: theme.GetAxisSplitLineColor(), SplitLineShow: true, } + if !yAxis.Color.IsZero() { + style.FontColor = yAxis.Color + style.StrokeColor = yAxis.Color + } width := NewAxis(p, data, style).measure().Width yAxisCount := len(opt.YAxisList)