diff --git a/axis.go b/axis.go
index cb4edc1..1086f7a 100644
--- a/axis.go
+++ b/axis.go
@@ -3,23 +3,21 @@ package charts
import (
"math"
"strings"
-
- "github.com/go-analyze/charts/chartdraw"
)
type axisPainter struct {
p *Painter
- opt *AxisOption
+ opt *axisOption
}
-func NewAxisPainter(p *Painter, opt AxisOption) *axisPainter {
+func newAxisPainter(p *Painter, opt axisOption) *axisPainter {
return &axisPainter{
p: p,
opt: &opt,
}
}
-type AxisOption struct {
+type axisOption struct {
// Show specifies if the axis should be rendered, set this to *false (through False()) to hide the axis.
Show *bool
// Theme specifies the colors used for the axis.
@@ -97,22 +95,7 @@ func (a *axisPainter) Render() (Box, error) {
tickLength := getDefaultInt(opt.TickLength, 5)
labelMargin := getDefaultInt(opt.LabelMargin, 5)
- style := chartdraw.Style{
- StrokeColor: theme.GetAxisStrokeColor(),
- StrokeWidth: strokeWidth,
- FontStyle: fontStyle,
- }
- top.SetDrawingStyle(style).OverrideFontStyle(style.FontStyle)
-
- isTextRotation := opt.TextRotation != 0
-
- if isTextRotation {
- top.SetTextRotation(opt.TextRotation)
- }
- textMaxWidth, textMaxHeight := top.MeasureTextMaxWidthHeight(opt.Data)
- if isTextRotation {
- top.ClearTextRotation()
- }
+ textMaxWidth, textMaxHeight := top.measureTextMaxWidthHeight(opt.Data, opt.TextRotation, fontStyle)
width := 0
height := 0
@@ -221,27 +204,30 @@ func (a *axisPainter) Render() (Box, error) {
} else {
// there is always one more tick than data sample, and if we are centering labels we use that extra tick to
// center the label against, if not centering then we need one less tick spacing
- // passing the tickSpaces reduces the need to copy the logic from painter.go:MultiText
+ // passing the tickSpaces reduces the need to copy the logic from painter.go:multiText
tickSpaces--
}
if strokeWidth > 0 {
+ strokeColor := theme.GetAxisStrokeColor()
p.Child(PainterPaddingOption(Box{
Top: ticksPaddingTop,
Left: ticksPaddingLeft,
IsSet: true,
- })).Ticks(TicksOption{
- LabelCount: labelCount,
- TickCount: tickCount,
- TickSpaces: tickSpaces,
- Length: tickLength,
- Vertical: isVertical,
- First: opt.DataStartIndex,
+ })).ticks(ticksOption{
+ labelCount: labelCount,
+ tickCount: tickCount,
+ tickSpaces: tickSpaces,
+ length: tickLength,
+ vertical: isVertical,
+ firstIndex: opt.DataStartIndex,
+ strokeWidth: strokeWidth,
+ strokeColor: strokeColor,
})
p.LineStroke([]Point{
{X: x0, Y: y0},
{X: x1, Y: y1},
- })
+ }, strokeColor, strokeWidth)
}
p.Child(PainterPaddingOption(Box{
@@ -249,23 +235,21 @@ func (a *axisPainter) Render() (Box, error) {
Top: labelPaddingTop,
Right: labelPaddingRight,
IsSet: true,
- })).MultiText(MultiTextOption{
- First: opt.DataStartIndex,
- Align: textAlign,
- TextList: opt.Data,
- Vertical: isVertical,
- LabelCount: labelCount,
- TickCount: tickCount,
- LabelSkipCount: opt.LabelSkipCount,
- CenterLabels: centerLabels,
- TextRotation: opt.TextRotation,
- Offset: opt.LabelOffset,
+ })).multiText(multiTextOption{
+ firstIndex: opt.DataStartIndex,
+ align: textAlign,
+ textList: opt.Data,
+ fontStyle: fontStyle,
+ vertical: isVertical,
+ labelCount: labelCount,
+ tickCount: tickCount,
+ labelSkipCount: opt.LabelSkipCount,
+ centerLabels: centerLabels,
+ textRotation: opt.TextRotation,
+ offset: opt.LabelOffset,
})
if opt.SplitLineShow { // show auxiliary lines
- style.StrokeColor = theme.GetAxisSplitLineColor()
- style.StrokeWidth = 1
- top.OverrideDrawingStyle(style)
if isVertical {
x0 := p.Width()
x1 := top.Width()
@@ -279,7 +263,7 @@ func (a *axisPainter) Render() (Box, error) {
top.LineStroke([]Point{
{X: x0, Y: y},
{X: x1, Y: y},
- })
+ }, theme.GetAxisSplitLineColor(), 1)
}
} else {
y0 := p.Height() - defaultXAxisHeight
@@ -292,7 +276,7 @@ func (a *axisPainter) Render() (Box, error) {
top.LineStroke([]Point{
{X: x, Y: y0},
{X: x, Y: y1},
- })
+ }, theme.GetAxisSplitLineColor(), 1)
}
}
}
diff --git a/axis_test.go b/axis_test.go
index 35ad4be..8007c89 100644
--- a/axis_test.go
+++ b/axis_test.go
@@ -6,6 +6,7 @@ import (
"github.com/stretchr/testify/require"
+ "github.com/go-analyze/charts/chartdraw"
"github.com/go-analyze/charts/chartdraw/drawing"
)
@@ -13,151 +14,152 @@ func TestAxis(t *testing.T) {
t.Parallel()
dayLabels := []string{
- "Mon",
- "Tue",
- "Wed",
- "Thu",
- "Fri",
- "Sat",
- "Sun",
+ "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun",
}
letterLabels := []string{"A", "B", "C", "D", "E", "F", "G"}
tests := []struct {
- name string
- render func(*Painter) ([]byte, error)
- result string
+ name string
+ optionFactory func() axisOption
+ padPainter bool
+ result string
}{
{
- name: "x-axis_bottom",
- render: func(p *Painter) ([]byte, error) {
- _, _ = NewAxisPainter(p, AxisOption{
+ name: "x-axis_bottom",
+ padPainter: true,
+ optionFactory: func() axisOption {
+ opt := XAxisOption{
+ Data: dayLabels,
+ BoundaryGap: True(),
+ FontStyle: FontStyle{
+ FontSize: 18,
+ },
+ }
+ return opt.toAxisOption()
+ },
+ result: "",
+ },
+ {
+ name: "x-axis_bottom_splitline",
+ optionFactory: func() axisOption {
+ return axisOption{
Data: dayLabels,
SplitLineShow: true,
- }).Render()
- return p.Bytes()
+ }
},
- result: "",
+ result: "",
},
{
name: "x-axis_bottom_left",
- render: func(p *Painter) ([]byte, error) {
- _, _ = NewAxisPainter(p, AxisOption{
+ optionFactory: func() axisOption {
+ return axisOption{
Data: dayLabels,
BoundaryGap: False(),
- }).Render()
- return p.Bytes()
+ }
},
- result: "",
+ result: "",
},
{
name: "y-axis_left",
- render: func(p *Painter) ([]byte, error) {
- _, _ = NewAxisPainter(p, AxisOption{
- Data: dayLabels,
- Position: PositionLeft,
- }).Render()
- return p.Bytes()
+ optionFactory: func() axisOption {
+ opt := YAxisOption{
+ Data: dayLabels,
+ Position: PositionLeft,
+ isCategoryAxis: true,
+ }
+ return opt.toAxisOption(nil)
},
- result: "",
+ result: "",
},
{
name: "y-axis_center",
- render: func(p *Painter) ([]byte, error) {
- _, _ = NewAxisPainter(p, AxisOption{
+ optionFactory: func() axisOption {
+ return axisOption{
Data: dayLabels,
Position: PositionLeft,
BoundaryGap: False(),
SplitLineShow: true,
- }).Render()
- return p.Bytes()
+ }
},
- result: "",
+ result: "",
},
{
name: "y-axis_right",
- render: func(p *Painter) ([]byte, error) {
- _, _ = NewAxisPainter(p, AxisOption{
+ optionFactory: func() axisOption {
+ return axisOption{
Data: dayLabels,
Position: PositionRight,
BoundaryGap: False(),
SplitLineShow: true,
- }).Render()
- return p.Bytes()
+ }
},
- result: "",
+ result: "",
},
{
name: "top",
- render: func(p *Painter) ([]byte, error) {
- _, _ = NewAxisPainter(p, AxisOption{
+ optionFactory: func() axisOption {
+ return axisOption{
Data: dayLabels,
Formatter: "{value} --",
Position: PositionTop,
- }).Render()
- return p.Bytes()
+ }
},
- result: "",
+ result: "",
},
{
name: "reduced_label_count",
- render: func(p *Painter) ([]byte, error) {
- _, _ = NewAxisPainter(p, AxisOption{
+ optionFactory: func() axisOption {
+ return axisOption{
Data: letterLabels,
SplitLineShow: false,
LabelCountAdjustment: -1,
- }).Render()
- return p.Bytes()
+ }
},
- result: "",
+ result: "",
},
{
name: "custom_unit",
- render: func(p *Painter) ([]byte, error) {
- _, _ = NewAxisPainter(p, AxisOption{
+ optionFactory: func() axisOption {
+ return axisOption{
Data: letterLabels,
SplitLineShow: false,
Unit: 10,
- }).Render()
- return p.Bytes()
+ }
},
- result: "",
+ result: "",
},
{
name: "custom_font",
- render: func(p *Painter) ([]byte, error) {
- _, _ = NewAxisPainter(p, AxisOption{
+ optionFactory: func() axisOption {
+ return axisOption{
Data: letterLabels,
FontStyle: FontStyle{
FontSize: 40.0,
FontColor: drawing.ColorBlue,
},
- }).Render()
- return p.Bytes()
+ }
},
- result: "",
+ result: "",
},
{
name: "boundary_gap_disable",
- render: func(p *Painter) ([]byte, error) {
- _, _ = NewAxisPainter(p, AxisOption{
+ optionFactory: func() axisOption {
+ return axisOption{
Data: letterLabels,
BoundaryGap: False(),
- }).Render()
- return p.Bytes()
+ }
},
- result: "",
+ result: "",
},
{
name: "boundary_gap_enable",
- render: func(p *Painter) ([]byte, error) {
- _, _ = NewAxisPainter(p, AxisOption{
+ optionFactory: func() axisOption {
+ return axisOption{
Data: letterLabels,
BoundaryGap: True(),
- }).Render()
- return p.Bytes()
+ }
},
- result: "",
+ result: "",
},
}
@@ -170,13 +172,18 @@ func TestAxis(t *testing.T) {
})
for i, tt := range tests {
t.Run(strconv.Itoa(i)+"-"+tt.name, func(t *testing.T) {
- p, err := NewPainter(PainterOptions{
+ p := NewPainter(PainterOptions{
OutputFormat: ChartOutputSVG,
Width: 600,
Height: 400,
}, PainterThemeOption(axisTheme))
+ if tt.padPainter {
+ p = p.Child(PainterPaddingOption(chartdraw.NewBox(50, 50, 50, 50)))
+ }
+
+ _, err := newAxisPainter(p, tt.optionFactory()).Render()
require.NoError(t, err)
- data, err := tt.render(p)
+ data, err := p.Bytes()
require.NoError(t, err)
assertEqualSVG(t, tt.result, data)
})
diff --git a/bar_chart.go b/bar_chart.go
index 7a690ac..83cbdce 100644
--- a/bar_chart.go
+++ b/bar_chart.go
@@ -5,8 +5,6 @@ import (
"math"
"github.com/golang/freetype/truetype"
-
- "github.com/go-analyze/charts/chartdraw"
)
type barChart struct {
@@ -14,14 +12,27 @@ type barChart struct {
opt *BarChartOption
}
-// NewBarChart returns a bar chart renderer
-func NewBarChart(p *Painter, opt BarChartOption) *barChart {
+// newBarChart returns a bar chart renderer
+func newBarChart(p *Painter, opt BarChartOption) *barChart {
return &barChart{
p: p,
opt: &opt,
}
}
+// NewBarChartOptionWithData returns an initialized BarChartOption with the SeriesList set for the provided data slice.
+func NewBarChartOptionWithData(data [][]float64) BarChartOption {
+ sl := NewSeriesListBar(data)
+ return BarChartOption{
+ SeriesList: sl,
+ Padding: defaultPadding,
+ Theme: GetDefaultTheme(),
+ Font: GetDefaultFont(),
+ YAxis: make([]YAxisOption, sl.getYAxisCount()),
+ ValueFormatter: defaultValueFormatter,
+ }
+}
+
type BarChartOption struct {
// Theme specifies the colors used for the bar chart.
Theme ColorPalette
@@ -43,6 +54,8 @@ type BarChartOption struct {
BarWidth int
// RoundedBarCaps set to `true` to produce a bar graph where the bars have rounded tops.
RoundedBarCaps *bool
+ // ValueFormatter defines how float values should be rendered to strings, notably for numeric axis labels.
+ ValueFormatter ValueFormatter
}
func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) {
@@ -50,7 +63,8 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B
opt := b.opt
seriesPainter := result.seriesPainter
- xRange := NewRange(b.p, seriesPainter.Width(), len(opt.XAxis.Data), 0.0, 0.0, 0.0, 0.0)
+ xRange := newRange(b.p, getPreferredValueFormatter(opt.XAxis.ValueFormatter, opt.ValueFormatter),
+ seriesPainter.Width(), len(opt.XAxis.Data), 0.0, 0.0, 0.0, 0.0)
x0, x1 := xRange.GetRange(0)
width := int(x1 - x0)
// margin between each block
@@ -78,9 +92,9 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B
theme := opt.Theme
seriesNames := seriesList.Names()
- markPointPainter := NewMarkPointPainter(seriesPainter)
- markLinePainter := NewMarkLinePainter(seriesPainter)
- rendererList := []Renderer{
+ markPointPainter := newMarkPointPainter(seriesPainter)
+ markLinePainter := newMarkLinePainter(seriesPainter)
+ rendererList := []renderer{
markPointPainter,
markLinePainter,
}
@@ -91,15 +105,9 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B
divideValues := xRange.AutoDivide()
points := make([]Point, len(series.Data))
- var labelPainter *SeriesLabelPainter
- if series.Label.Show {
- labelPainter = NewSeriesLabelPainter(SeriesLabelPainterParams{
- P: seriesPainter,
- SeriesNames: seriesNames,
- Label: series.Label,
- Theme: opt.Theme,
- Font: opt.Font,
- })
+ var labelPainter *seriesLabelPainter
+ if flagIs(true, series.Label.Show) {
+ labelPainter = newSeriesLabelPainter(seriesPainter, seriesNames, series.Label, opt.Theme, opt.Font)
rendererList = append(rendererList, labelPainter)
}
@@ -113,29 +121,26 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B
x += index * (barWidth + barMargin)
}
- h := yRange.getHeight(item.Value)
- fillColor := seriesColor
+ h := yRange.getHeight(item)
top := barMaxHeight - h
- seriesPainter.OverrideDrawingStyle(chartdraw.Style{
- FillColor: fillColor,
- })
if flagIs(true, opt.RoundedBarCaps) {
- seriesPainter.RoundedRect(Box{
+ seriesPainter.roundedRect(Box{
Top: top,
Left: x,
Right: x + barWidth,
Bottom: barMaxHeight - 1,
IsSet: true,
- }, barWidth, true, false)
+ }, barWidth, true, false,
+ seriesColor, seriesColor, 0.0)
} else {
- seriesPainter.Rect(Box{
+ seriesPainter.filledRect(Box{
Top: top,
Left: x,
Right: x + barWidth,
Bottom: barMaxHeight - 1,
IsSet: true,
- })
+ }, seriesColor, seriesColor, 0.0)
}
// generate marker point by hand
points[j] = Point{
@@ -153,23 +158,22 @@ func (b *barChart) render(result *defaultRenderResult, seriesList SeriesList) (B
y = barMaxHeight
radians = -math.Pi / 2
if fontStyle.FontColor.IsZero() {
- if isLightColor(fillColor) {
+ if isLightColor(seriesColor) {
fontStyle.FontColor = defaultLightFontColor
} else {
fontStyle.FontColor = defaultDarkFontColor
}
}
}
- labelPainter.Add(LabelValue{
- Vertical: true, // label is above bar
- Index: index,
- Value: item.Value,
- FontStyle: fontStyle,
- X: x + barWidth>>1,
- Y: y,
- // rotate
- Radians: radians,
- Offset: series.Label.Offset,
+ labelPainter.Add(labelValue{
+ vertical: true, // label is above bar
+ index: index,
+ value: item,
+ fontStyle: fontStyle,
+ x: x + (barWidth >> 1),
+ y: y,
+ radians: radians,
+ offset: series.Label.Offset,
})
}
@@ -203,13 +207,14 @@ func (b *barChart) Render() (Box, error) {
opt.Theme = getPreferredTheme(p.theme)
}
renderResult, err := defaultRender(p, defaultRenderOption{
- Theme: opt.Theme,
- Padding: opt.Padding,
- SeriesList: opt.SeriesList,
- XAxis: opt.XAxis,
- YAxis: opt.YAxis,
- Title: opt.Title,
- Legend: opt.Legend,
+ theme: opt.Theme,
+ padding: opt.Padding,
+ seriesList: opt.SeriesList,
+ xAxis: &b.opt.XAxis,
+ yAxis: opt.YAxis,
+ title: opt.Title,
+ legend: &b.opt.Legend,
+ valueFormatter: opt.ValueFormatter,
})
if err != nil {
return BoxZero, err
diff --git a/bar_chart_test.go b/bar_chart_test.go
index 30b9286..f09e92d 100644
--- a/bar_chart_test.go
+++ b/bar_chart_test.go
@@ -4,18 +4,19 @@ import (
"strconv"
"testing"
+ "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/go-analyze/charts/chartdraw/drawing"
)
func makeBasicBarChartOption() BarChartOption {
- seriesList := NewSeriesListDataFromValues([][]float64{
+ seriesList := NewSeriesListBar([][]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},
{2.6, 5.9, 9.0, 26.4, 28.7, 70.7, 175.6, 182.2, 48.7, 18.8, 6.0, 2.3},
- }, ChartTypeBar)
+ })
for index := range seriesList {
- seriesList[index].Label.Show = true
+ seriesList[index].Label.Show = True()
}
return BarChartOption{
Padding: Box{
@@ -40,6 +41,23 @@ func makeBasicBarChartOption() BarChartOption {
}
}
+func TestNewBarChartOptionWithData(t *testing.T) {
+ t.Parallel()
+
+ opt := NewBarChartOptionWithData([][]float64{
+ {12, 24},
+ {24, 48},
+ })
+
+ assert.Len(t, opt.SeriesList, 2)
+ assert.Equal(t, ChartTypeBar, opt.SeriesList[0].Type)
+ assert.Len(t, opt.YAxis, 1)
+ assert.Equal(t, defaultPadding, opt.Padding)
+
+ p := NewPainter(PainterOptions{})
+ assert.NoError(t, p.BarChart(opt))
+}
+
func TestBarChart(t *testing.T) {
t.Parallel()
@@ -53,13 +71,13 @@ func TestBarChart(t *testing.T) {
name: "default",
defaultTheme: true,
makeOptions: makeBasicBarChartOption,
- result: "",
+ result: "",
},
{
name: "themed",
defaultTheme: false,
makeOptions: makeBasicBarChartOption,
- result: "",
+ result: "",
},
{
name: "rounded_caps",
@@ -69,7 +87,7 @@ func TestBarChart(t *testing.T) {
opt.RoundedBarCaps = True()
return opt
},
- result: "",
+ result: "",
},
{
name: "custom_font",
@@ -85,7 +103,7 @@ func TestBarChart(t *testing.T) {
opt.Title.FontStyle = customFont
return opt
},
- result: "",
+ result: "",
},
{
name: "boundary_gap_enable",
@@ -95,7 +113,7 @@ func TestBarChart(t *testing.T) {
opt.XAxis.BoundaryGap = True()
return opt
},
- result: "",
+ result: "",
},
{
name: "boundary_gap_disable",
@@ -105,7 +123,19 @@ func TestBarChart(t *testing.T) {
opt.XAxis.BoundaryGap = False()
return opt
},
- result: "",
+ result: "",
+ },
+ {
+ name: "value_formatter",
+ defaultTheme: true,
+ makeOptions: func() BarChartOption {
+ opt := makeBasicBarChartOption()
+ opt.ValueFormatter = func(f float64) string {
+ return "f"
+ }
+ return opt
+ },
+ result: "",
},
}
@@ -117,21 +147,18 @@ func TestBarChart(t *testing.T) {
}
if tt.defaultTheme {
t.Run(strconv.Itoa(i)+"-"+tt.name, func(t *testing.T) {
- p, err := NewPainter(painterOptions)
- require.NoError(t, err)
+ p := NewPainter(painterOptions)
validateBarChartRender(t, p, tt.makeOptions(), tt.result)
})
} else {
t.Run(strconv.Itoa(i)+"-"+tt.name+"-painter", func(t *testing.T) {
- p, err := NewPainter(painterOptions, PainterThemeOption(GetTheme(ThemeVividDark)))
- require.NoError(t, err)
+ p := NewPainter(painterOptions, PainterThemeOption(GetTheme(ThemeVividDark)))
validateBarChartRender(t, p, tt.makeOptions(), tt.result)
})
t.Run(strconv.Itoa(i)+"-"+tt.name+"-options", func(t *testing.T) {
- p, err := NewPainter(painterOptions)
- require.NoError(t, err)
+ p := NewPainter(painterOptions)
opt := tt.makeOptions()
opt.Theme = GetTheme(ThemeVividDark)
@@ -144,7 +171,7 @@ func TestBarChart(t *testing.T) {
func validateBarChartRender(t *testing.T, p *Painter, opt BarChartOption, expectedResult string) {
t.Helper()
- _, err := NewBarChart(p, opt).Render()
+ err := p.BarChart(opt)
require.NoError(t, err)
data, err := p.Bytes()
require.NoError(t, err)
diff --git a/chart_option.go b/chart_option.go
index 6009497..dd11cef 100644
--- a/chart_option.go
+++ b/chart_option.go
@@ -2,7 +2,6 @@ package charts
import (
"fmt"
- "sort"
"github.com/golang/freetype/truetype"
)
@@ -47,7 +46,7 @@ type ChartOption struct {
BarWidth int
// BarHeight is the height of the bars for horizontal bar charts.
BarHeight int
- // Children are child charts to render together.
+ // Children are Child charts to render together.
Children []ChartOption
parent *Painter
// ValueFormatter to format numeric values into labels.
@@ -57,18 +56,18 @@ type ChartOption struct {
// OptionFunc option function
type OptionFunc func(opt *ChartOption)
-// SVGOutputOption set svg type of chart's output.
-func SVGOutputOption() OptionFunc {
- return OutputFormatOptionFunc(ChartOutputSVG)
+// SVGOutputOptionFunc set svg type of chart's output.
+func SVGOutputOptionFunc() OptionFunc {
+ return outputFormatOptionFunc(ChartOutputSVG)
}
-// PNGOutputOption set png type of chart's output.
-func PNGOutputOption() OptionFunc {
- return OutputFormatOptionFunc(ChartOutputPNG)
+// PNGOutputOptionFunc set png type of chart's output.
+func PNGOutputOptionFunc() OptionFunc {
+ return outputFormatOptionFunc(ChartOutputPNG)
}
-// OutputFormatOptionFunc set type of chart's output.
-func OutputFormatOptionFunc(t string) OptionFunc {
+// outputFormatOptionFunc set type of chart's output.
+func outputFormatOptionFunc(t string) OptionFunc {
return func(opt *ChartOption) {
opt.OutputFormat = t
}
@@ -162,16 +161,10 @@ func YAxisDataOptionFunc(data []string) OptionFunc {
}
}
-// WidthOptionFunc set width of chart
-func WidthOptionFunc(width int) OptionFunc {
+// DimensionsOptionFunc sets the width and height dimensions of the chart.
+func DimensionsOptionFunc(width, height int) OptionFunc {
return func(opt *ChartOption) {
opt.Width = width
- }
-}
-
-// HeightOptionFunc set height of chart
-func HeightOptionFunc(height int) OptionFunc {
- return func(opt *ChartOption) {
opt.Height = height
}
}
@@ -183,23 +176,16 @@ func PaddingOptionFunc(padding Box) OptionFunc {
}
}
-// BoxOptionFunc set box of chart
-func BoxOptionFunc(box Box) OptionFunc {
- return func(opt *ChartOption) {
- opt.Box = box
- }
-}
-
-// PieSeriesShowLabel set pie series show label
-func PieSeriesShowLabel() OptionFunc {
+// SeriesShowLabel set the series show label state for all series.
+func SeriesShowLabel(show bool) OptionFunc {
return func(opt *ChartOption) {
for index := range opt.SeriesList {
- opt.SeriesList[index].Label.Show = true
+ opt.SeriesList[index].Label.Show = BoolPointer(show)
}
}
}
-// ChildOptionFunc add child chart
+// ChildOptionFunc adds a Child chart on top of the current one. Use Padding and Box for positioning.
func ChildOptionFunc(child ...ChartOption) OptionFunc {
return func(opt *ChartOption) {
if opt.Children == nil {
@@ -240,14 +226,9 @@ func (o *ChartOption) fillDefault() error {
o.Width = getDefaultInt(o.Width, defaultChartWidth)
o.Height = getDefaultInt(o.Height, defaultChartHeight)
- yaxisCount := 1
- for _, series := range o.SeriesList {
- if series.YAxisIndex == 1 {
- yaxisCount++
- break
- } else if series.YAxisIndex > 1 {
- return fmt.Errorf("series '%s' specified invalid y-axis index: %v", series.Name, series.YAxisIndex)
- }
+ yaxisCount := o.SeriesList.getYAxisCount()
+ if yaxisCount < 0 {
+ return fmt.Errorf("series specified invalid y-axis index")
}
if len(o.YAxis) < yaxisCount {
yAxisOptions := make([]YAxisOption, yaxisCount)
@@ -278,31 +259,7 @@ func (o *ChartOption) fillDefault() error {
fillThemeDefaults(o.Theme, &o.Title, &o.Legend, &o.XAxis)
if o.Padding.IsZero() {
- o.Padding = Box{
- Top: 20,
- Right: 20,
- Bottom: 20,
- Left: 20,
- }
- }
- // association between legend and series name
- if len(o.Legend.Data) == 0 {
- o.Legend.Data = o.SeriesList.Names()
- } else {
- seriesCount := len(o.SeriesList)
- for index, name := range o.Legend.Data {
- if index < seriesCount && len(o.SeriesList[index].Name) == 0 {
- o.SeriesList[index].Name = name
- }
- }
- nameIndexDict := map[string]int{}
- for index, name := range o.Legend.Data {
- nameIndexDict[name] = index
- }
- // ensure order of series is consistent with legend
- sort.Slice(o.SeriesList, func(i, j int) bool {
- return nameIndexDict[o.SeriesList[i].Name] < nameIndexDict[o.SeriesList[j].Name]
- })
+ o.Padding = defaultPadding
}
return nil
}
@@ -322,100 +279,41 @@ func fillThemeDefaults(defaultTheme ColorPalette, title *TitleOption, legend *Le
// LineRender line chart render
func LineRender(values [][]float64, opts ...OptionFunc) (*Painter, error) {
return Render(ChartOption{
- SeriesList: NewSeriesListDataFromValues(values, ChartTypeLine),
+ SeriesList: NewSeriesListLine(values),
}, opts...)
}
// BarRender bar chart render
func BarRender(values [][]float64, opts ...OptionFunc) (*Painter, error) {
return Render(ChartOption{
- SeriesList: NewSeriesListDataFromValues(values, ChartTypeBar),
+ SeriesList: NewSeriesListBar(values),
}, opts...)
}
// HorizontalBarRender horizontal bar chart render
func HorizontalBarRender(values [][]float64, opts ...OptionFunc) (*Painter, error) {
return Render(ChartOption{
- SeriesList: NewSeriesListDataFromValues(values, ChartTypeHorizontalBar),
+ SeriesList: NewSeriesListHorizontalBar(values),
}, opts...)
}
// PieRender pie chart render
func PieRender(values []float64, opts ...OptionFunc) (*Painter, error) {
return Render(ChartOption{
- SeriesList: NewPieSeriesList(values),
+ SeriesList: NewSeriesListPie(values),
}, opts...)
}
// RadarRender radar chart render
func RadarRender(values [][]float64, opts ...OptionFunc) (*Painter, error) {
return Render(ChartOption{
- SeriesList: NewSeriesListDataFromValues(values, ChartTypeRadar),
+ SeriesList: NewSeriesListRadar(values),
}, opts...)
}
// FunnelRender funnel chart render
func FunnelRender(values []float64, opts ...OptionFunc) (*Painter, error) {
return Render(ChartOption{
- SeriesList: NewFunnelSeriesList(values),
+ SeriesList: NewSeriesListFunnel(values),
}, opts...)
}
-
-// TableRender table chart render
-func TableRender(header []string, data [][]string, spanMaps ...map[int]int) (*Painter, error) {
- opt := TableChartOption{
- Header: header,
- Data: data,
- }
- if len(spanMaps) != 0 {
- spanMap := spanMaps[0]
- spans := make([]int, len(opt.Header))
- for index := range spans {
- v, ok := spanMap[index]
- if !ok {
- v = 1
- }
- spans[index] = v
- }
- opt.Spans = spans
- }
- return TableOptionRender(opt)
-}
-
-// TableOptionRender table render with option
-func TableOptionRender(opt TableChartOption) (*Painter, error) {
- if opt.OutputFormat == "" {
- opt.OutputFormat = chartDefaultOutputFormat
- }
- if opt.Width <= 0 {
- opt.Width = defaultChartWidth
- }
-
- p, err := NewPainter(PainterOptions{
- OutputFormat: opt.OutputFormat,
- Width: opt.Width,
- Height: 100, // is only used to calculate the height of the table
- Font: opt.FontStyle.Font,
- })
- if err != nil {
- return nil, err
- }
- info, err := NewTableChart(p, opt).render()
- if err != nil {
- return nil, err
- }
-
- p, err = NewPainter(PainterOptions{
- OutputFormat: opt.OutputFormat,
- Width: info.width,
- Height: info.height,
- Font: opt.FontStyle.Font,
- })
- if err != nil {
- return nil, err
- }
- if _, err = NewTableChart(p, opt).renderWithInfo(info); err != nil {
- return nil, err
- }
- return p, nil
-}
diff --git a/chart_option_test.go b/chart_option_test.go
index f65ea61..78d20c8 100644
--- a/chart_option_test.go
+++ b/chart_option_test.go
@@ -1,6 +1,7 @@
package charts
import (
+ "fmt"
"testing"
"github.com/stretchr/testify/assert"
@@ -13,15 +14,14 @@ func TestChartOption(t *testing.T) {
t.Parallel()
fns := []OptionFunc{
- SVGOutputOption(),
+ SVGOutputOptionFunc(),
FontOptionFunc(GetDefaultFont()),
ThemeNameOptionFunc(ThemeVividDark),
TitleTextOptionFunc("title"),
LegendLabelsOptionFunc([]string{"label"}),
XAxisDataOptionFunc([]string{"xaxis"}),
YAxisDataOptionFunc([]string{"yaxis"}),
- WidthOptionFunc(800),
- HeightOptionFunc(600),
+ DimensionsOptionFunc(800, 600),
PaddingOptionFunc(Box{
Left: 10,
Top: 10,
@@ -62,21 +62,29 @@ func TestChartOption(t *testing.T) {
}, opt)
}
-func TestChartOptionPieSeriesShowLabel(t *testing.T) {
+func TestChartOptionSeriesShowLabel(t *testing.T) {
t.Parallel()
opt := ChartOption{
- SeriesList: NewPieSeriesList([]float64{1, 2}),
+ SeriesList: NewSeriesListPie([]float64{1, 2}),
}
- PieSeriesShowLabel()(&opt)
- assert.True(t, opt.SeriesList[0].Label.Show)
+ SeriesShowLabel(true)(&opt)
+ assert.True(t, flagIs(true, opt.SeriesList[0].Label.Show))
+
+ SeriesShowLabel(false)(&opt)
+ assert.True(t, flagIs(false, opt.SeriesList[0].Label.Show))
+}
+
+func newNoTypeSeriesListFromValues(values [][]float64) SeriesList {
+ return newSeriesListFromValues(values, "",
+ SeriesLabel{}, nil, "", SeriesMarkPoint{}, SeriesMarkLine{})
}
func TestChartOptionMarkLine(t *testing.T) {
t.Parallel()
opt := ChartOption{
- SeriesList: NewSeriesListDataFromValues([][]float64{{1, 2}}),
+ SeriesList: newNoTypeSeriesListFromValues([][]float64{{1, 2}}),
}
MarkLineOptionFunc(0, "min", "max")(&opt)
assert.Equal(t, NewMarkLine("min", "max"), opt.SeriesList[0].MarkLine)
@@ -86,7 +94,7 @@ func TestChartOptionMarkPoint(t *testing.T) {
t.Parallel()
opt := ChartOption{
- SeriesList: NewSeriesListDataFromValues([][]float64{{1, 2}}),
+ SeriesList: newNoTypeSeriesListFromValues([][]float64{{1, 2}}),
}
MarkPointOptionFunc(0, "min", "max")(&opt)
assert.Equal(t, NewMarkPoint("min", "max"), opt.SeriesList[0].MarkPoint)
@@ -104,7 +112,7 @@ func TestLineRender(t *testing.T) {
}
p, err := LineRender(
values,
- SVGOutputOption(),
+ SVGOutputOptionFunc(),
TitleTextOptionFunc("Line"),
XAxisDataOptionFunc([]string{
"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun",
@@ -112,11 +120,16 @@ func TestLineRender(t *testing.T) {
LegendLabelsOptionFunc([]string{
"Email", "Union Ads", "Video Ads", "Direct", "Search Engine",
}),
+ func(opt *ChartOption) {
+ opt.ValueFormatter = func(f float64) string {
+ return fmt.Sprintf("%.0f", f)
+ }
+ },
)
require.NoError(t, err)
data, err := p.Bytes()
require.NoError(t, err)
- assertEqualSVG(t, "", data)
+ assertEqualSVG(t, "", data)
}
func TestBarRender(t *testing.T) {
@@ -128,7 +141,7 @@ func TestBarRender(t *testing.T) {
}
p, err := BarRender(
values,
- SVGOutputOption(),
+ SVGOutputOptionFunc(),
XAxisDataOptionFunc([]string{
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
}),
@@ -153,7 +166,7 @@ func TestBarRender(t *testing.T) {
require.NoError(t, err)
data, err := p.Bytes()
require.NoError(t, err)
- assertEqualSVG(t, "", data)
+ assertEqualSVG(t, "", data)
}
func TestHorizontalBarRender(t *testing.T) {
@@ -165,7 +178,7 @@ func TestHorizontalBarRender(t *testing.T) {
}
p, err := HorizontalBarRender(
values,
- SVGOutputOption(),
+ SVGOutputOptionFunc(),
TitleTextOptionFunc("World Population"),
PaddingOptionFunc(Box{
Top: 20,
@@ -183,7 +196,7 @@ func TestHorizontalBarRender(t *testing.T) {
require.NoError(t, err)
data, err := p.Bytes()
require.NoError(t, err)
- assertEqualSVG(t, "", data)
+ assertEqualSVG(t, "", data)
}
func TestPieRender(t *testing.T) {
@@ -192,7 +205,7 @@ func TestPieRender(t *testing.T) {
values := []float64{1048, 735, 580, 484, 300}
p, err := PieRender(
values,
- SVGOutputOption(),
+ SVGOutputOptionFunc(),
TitleOptionFunc(TitleOption{
Text: "Rainfall vs Evaporation",
Subtext: "Fake Data",
@@ -205,18 +218,17 @@ func TestPieRender(t *testing.T) {
Left: 20,
}),
LegendOptionFunc(LegendOption{
- Vertical: true,
+ Vertical: True(),
Data: []string{
"Search Engine", "Direct", "Email", "Union Ads", "Video Ads",
},
Offset: OffsetLeft,
}),
- PieSeriesShowLabel(),
)
require.NoError(t, err)
data, err := p.Bytes()
require.NoError(t, err)
- assertEqualSVG(t, "", data)
+ assertEqualSVG(t, "", data)
}
func TestRadarRender(t *testing.T) {
@@ -228,7 +240,7 @@ func TestRadarRender(t *testing.T) {
}
p, err := RadarRender(
values,
- SVGOutputOption(),
+ SVGOutputOptionFunc(),
TitleTextOptionFunc("Basic Radar Chart"),
LegendLabelsOptionFunc([]string{
"Allocated Budget", "Actual Spending",
@@ -247,7 +259,7 @@ func TestRadarRender(t *testing.T) {
require.NoError(t, err)
data, err := p.Bytes()
require.NoError(t, err)
- assertEqualSVG(t, "", data)
+ assertEqualSVG(t, "", data)
}
func TestFunnelRender(t *testing.T) {
@@ -258,7 +270,7 @@ func TestFunnelRender(t *testing.T) {
}
p, err := FunnelRender(
values,
- SVGOutputOption(),
+ SVGOutputOptionFunc(),
TitleTextOptionFunc("Funnel"),
LegendLabelsOptionFunc([]string{
"Show", "Click", "Visit", "Inquiry", "Order",
@@ -267,7 +279,7 @@ func TestFunnelRender(t *testing.T) {
require.NoError(t, err)
data, err := p.Bytes()
require.NoError(t, err)
- assertEqualSVG(t, "", data)
+ assertEqualSVG(t, "", data)
}
func TestChildRender(t *testing.T) {
@@ -277,16 +289,16 @@ func TestChildRender(t *testing.T) {
{150, 232, 201, 154, 190, 330, 410},
{320, 332, 301, 334, 390, 330, 320},
},
- SVGOutputOption(),
+ SVGOutputOptionFunc(),
XAxisDataOptionFunc([]string{
"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun",
}),
ChildOptionFunc(ChartOption{
Box: chartdraw.NewBox(10, 200, 500, 200),
- SeriesList: NewSeriesListDataFromValues([][]float64{
+ SeriesList: NewSeriesListHorizontalBar([][]float64{
{70, 90, 110, 130},
{80, 100, 120, 140},
- }, ChartTypeHorizontalBar),
+ }),
Legend: LegendOption{
Data: []string{
"2011", "2012",
@@ -304,5 +316,5 @@ func TestChildRender(t *testing.T) {
require.NoError(t, err)
data, err := p.Bytes()
require.NoError(t, err)
- assertEqualSVG(t, "", data)
+ assertEqualSVG(t, "", data)
}
diff --git a/chartdraw/annotation_series_test.go b/chartdraw/annotation_series_test.go
index 8abb1f0..ce878b5 100644
--- a/chartdraw/annotation_series_test.go
+++ b/chartdraw/annotation_series_test.go
@@ -5,7 +5,6 @@ import (
"testing"
"github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
"github.com/go-analyze/charts/chartdraw/drawing"
)
@@ -22,8 +21,7 @@ func TestAnnotationSeriesMeasure(t *testing.T) {
},
}
- r, err := PNG(110, 110)
- require.NoError(t, err)
+ r := PNG(110, 110)
xrange := &ContinuousRange{
Min: 1.0,
@@ -73,8 +71,7 @@ func TestAnnotationSeriesRender(t *testing.T) {
},
}
- r, err := PNG(110, 110)
- require.NoError(t, err)
+ r := PNG(110, 110)
xrange := &ContinuousRange{
Min: 1.0,
diff --git a/chartdraw/bar_chart.go b/chartdraw/bar_chart.go
index 9fb6b51..2d1707c 100644
--- a/chartdraw/bar_chart.go
+++ b/chartdraw/bar_chart.go
@@ -94,10 +94,7 @@ func (bc BarChart) Render(rp RendererProvider, w io.Writer) error {
return errors.New("please provide at least one bar")
}
- r, err := rp(bc.GetWidth(), bc.GetHeight())
- if err != nil {
- return err
- }
+ r := rp(bc.GetWidth(), bc.GetHeight())
if bc.Font == nil {
bc.defaultFont = GetDefaultFont()
diff --git a/chartdraw/bar_chart_test.go b/chartdraw/bar_chart_test.go
index 6d1dc26..f0c233b 100644
--- a/chartdraw/bar_chart_test.go
+++ b/chartdraw/bar_chart_test.go
@@ -217,8 +217,7 @@ func TestBarChartGetAxesTicks(t *testing.T) {
},
}
- r, err := PNG(128, 128)
- require.NoError(t, err)
+ r := PNG(128, 128)
yr := bc.getRanges()
yf := bc.getValueFormatters()
diff --git a/chartdraw/chart.go b/chartdraw/chart.go
index 5031de6..b5452e1 100644
--- a/chartdraw/chart.go
+++ b/chartdraw/chart.go
@@ -80,10 +80,7 @@ func (c Chart) Render(rp RendererProvider, w io.Writer) error {
c.YAxisSecondary.AxisType = YAxisSecondary
- r, err := rp(c.GetWidth(), c.GetHeight())
- if err != nil {
- return err
- }
+ r := rp(c.GetWidth(), c.GetHeight())
if c.Font == nil {
c.defaultFont = GetDefaultFont()
@@ -99,8 +96,7 @@ func (c Chart) Render(rp RendererProvider, w io.Writer) error {
xr, yr, yra = c.setRangeDomains(canvasBox, xr, yr, yra)
- err = c.checkRanges(xr, yr, yra)
- if err != nil {
+ if err := c.checkRanges(xr, yr, yra); err != nil {
_ = r.Save(w)
return err
}
diff --git a/chartdraw/chart_test.go b/chartdraw/chart_test.go
index ca8e592..99effff 100644
--- a/chartdraw/chart_test.go
+++ b/chartdraw/chart_test.go
@@ -316,8 +316,7 @@ func TestChartHasAxes(t *testing.T) {
func TestChartGetAxesTicks(t *testing.T) {
t.Parallel()
- r, err := PNG(1024, 1024)
- require.NoError(t, err)
+ r := PNG(1024, 1024)
c := Chart{
XAxis: XAxis{
diff --git a/chartdraw/donut_chart.go b/chartdraw/donut_chart.go
index 4d2a453..351c8e0 100644
--- a/chartdraw/donut_chart.go
+++ b/chartdraw/donut_chart.go
@@ -71,10 +71,7 @@ func (pc DonutChart) Render(rp RendererProvider, w io.Writer) error {
return errors.New("please provide at least one value")
}
- r, err := rp(pc.GetWidth(), pc.GetHeight())
- if err != nil {
- return err
- }
+ r := rp(pc.GetWidth(), pc.GetHeight())
if pc.Font == nil {
pc.defaultFont = GetDefaultFont()
diff --git a/chartdraw/drawing/color.go b/chartdraw/drawing/color.go
index 7c2c32d..8e23c6d 100644
--- a/chartdraw/drawing/color.go
+++ b/chartdraw/drawing/color.go
@@ -12,36 +12,71 @@ import (
var (
// ColorTransparent is a fully transparent color.
ColorTransparent = Color{R: 255, G: 255, B: 255, A: 0}
- // ColorWhite is white.
+ // ColorWhite is R: 255, G: 255, B: 255.
ColorWhite = Color{R: 255, G: 255, B: 255, A: 255}
- // ColorBlack is black.
+ // ColorBlack is R: 0, G: 0, B: 0.
ColorBlack = Color{R: 0, G: 0, B: 0, A: 255}
- // ColorRed is red.
+ // ColorGray is R: 128, G: 128, B: 128,
+ ColorGray = Color{R: 128, G: 128, B: 128, A: 255}
+ // ColorRed is R: 255, G: 0, B: 0.
ColorRed = Color{R: 255, G: 0, B: 0, A: 255}
- // ColorGreen is green.
+ // ColorGreen is R: 0, G: 128, B: 0.
ColorGreen = Color{R: 0, G: 128, B: 0, A: 255}
- // ColorBlue is blue.
+ // ColorBlue is R: 0, G: 0, B: 255.
ColorBlue = Color{R: 0, G: 0, B: 255, A: 255}
- // ColorSilver is a known color.
+ // ColorSilver is R: 192, G: 192, B: 192.
ColorSilver = Color{R: 192, G: 192, B: 192, A: 255}
- // ColorMaroon is a known color.
+ // ColorMaroon is R: 128, G: 0, B: 0.
ColorMaroon = Color{R: 128, G: 0, B: 0, A: 255}
- // ColorPurple is a known color.
+ // ColorPurple is R: 128, G: 0, B: 128.
ColorPurple = Color{R: 128, G: 0, B: 128, A: 255}
- // ColorFuchsia is a known color.
+ // ColorFuchsia is R: 255, G: 0, B: 255.
ColorFuchsia = Color{R: 255, G: 0, B: 255, A: 255}
- // ColorLime is a known color.
+ // ColorLime is R: 0, G: 255, B: 0.
ColorLime = Color{R: 0, G: 255, B: 0, A: 255}
- // ColorOlive is a known color.
+ // ColorOlive is R: 128, G: 128, B: 0.
ColorOlive = Color{R: 128, G: 128, B: 0, A: 255}
- // ColorYellow is a known color.
+ // ColorYellow is R: 255, G: 255, B: 0.
ColorYellow = Color{R: 255, G: 255, B: 0, A: 255}
- // ColorNavy is a known color.
+ // ColorNavy is R: 0, G: 0, B: 128.
ColorNavy = Color{R: 0, G: 0, B: 128, A: 255}
- // ColorTeal is a known color.
+ // ColorTeal is R: 0, G: 128, B: 128.
ColorTeal = Color{R: 0, G: 128, B: 128, A: 255}
- // ColorAqua is a known color.
+ // ColorAqua is R: 0, G: 255, B: 255.
ColorAqua = Color{R: 0, G: 255, B: 255, A: 255}
+
+ // select extended colors
+
+ // ColorAzure is R: 240, G: 255, B: 255.
+ ColorAzure = Color{R: 240, G: 255, B: 255, A: 255}
+ // ColorBeige is R: 245, G: 245, B: 220.
+ ColorBeige = Color{R: 245, G: 245, B: 220, A: 255}
+ // ColorBrown is R: 165, G: 42, B: 42.
+ ColorBrown = Color{R: 165, G: 42, B: 42, A: 255}
+ // ColorChocolate is R: 210, G: 105, B: 30.
+ ColorChocolate = Color{R: 210, G: 105, B: 30, A: 255}
+ // ColorCoral is R: 255, G: 127, B: 80.
+ ColorCoral = Color{R: 255, G: 127, B: 80, A: 255}
+ // ColorGold is R: 255, G: 215, B: 0.
+ ColorGold = Color{R: 255, G: 215, B: 0, A: 255}
+ // ColorIndigo is R: 75, G: 0, B: 130.
+ ColorIndigo = Color{R: 75, G: 0, B: 130, A: 255}
+ // ColorIvory is R: 255, G: 255, B: 250.
+ ColorIvory = Color{R: 255, G: 255, B: 250, A: 255}
+ // ColorOrange is R: 255, G: 165, B: 0.
+ ColorOrange = Color{R: 255, G: 165, B: 0, A: 255}
+ // ColorPink is R: 255, G: 192, B: 203.
+ ColorPink = Color{R: 255, G: 192, B: 203, A: 255}
+ // ColorPlum is R: 221, G: 160, B: 221.
+ ColorPlum = Color{R: 221, G: 160, B: 221, A: 255}
+ // ColorSalmon is R: 250, G: 128, B: 114.
+ ColorSalmon = Color{R: 250, G: 128, B: 114, A: 255}
+ // ColorTan is R: 210, G: 180, B: 140.
+ ColorTan = Color{R: 210, G: 180, B: 140, A: 255}
+ // ColorTurquoise is R: 64, G: 224, B: 208.
+ ColorTurquoise = Color{R: 64, G: 224, B: 208, A: 255}
+ // ColorViolet is R: 238, G: 130, B: 238.
+ ColorViolet = Color{R: 238, G: 130, B: 238, A: 255}
)
// ParseColor parses a color from a string.
@@ -129,6 +164,8 @@ func ColorFromKnown(known string) Color {
return ColorWhite
case "black":
return ColorBlack
+ case "grey", "gray":
+ return ColorGray
case "red":
return ColorRed
case "blue":
@@ -153,8 +190,38 @@ func ColorFromKnown(known string) Color {
return ColorNavy
case "teal":
return ColorTeal
- case "aqua":
+ case "cyan", "aqua":
return ColorAqua
+ case "azure":
+ return ColorAzure
+ case "beige":
+ return ColorBeige
+ case "brown":
+ return ColorBrown
+ case "chocolate":
+ return ColorChocolate
+ case "coral":
+ return ColorCoral
+ case "gold":
+ return ColorGold
+ case "indigo":
+ return ColorIndigo
+ case "ivory":
+ return ColorIvory
+ case "orange":
+ return ColorOrange
+ case "pink":
+ return ColorPink
+ case "plum":
+ return ColorPlum
+ case "salmon":
+ return ColorSalmon
+ case "tan":
+ return ColorTan
+ case "turquoise":
+ return ColorTurquoise
+ case "violet":
+ return ColorViolet
default:
return Color{}
}
@@ -235,6 +302,53 @@ func (c Color) AverageWith(other Color) Color {
// String returns a css string representation of the color.
func (c Color) String() string {
+ switch c {
+ case ColorWhite:
+ return "white"
+ case ColorBlack:
+ return "black"
+ case ColorRed:
+ return "red"
+ case ColorBlue:
+ return "blue"
+ case ColorGreen:
+ return "green"
+ case ColorSilver:
+ return "silver"
+ case ColorMaroon:
+ return "maroon"
+ case ColorPurple:
+ return "purple"
+ case ColorFuchsia:
+ return "fuchsia"
+ case ColorLime:
+ return "lime"
+ case ColorOlive:
+ return "olive"
+ case ColorYellow:
+ return "yellow"
+ case ColorNavy:
+ return "navy"
+ case ColorTeal:
+ return "teal"
+ case ColorAqua:
+ return "aqua"
+ default:
+ if c.A == 255 {
+ return c.StringRGB()
+ } else {
+ return c.StringRGBA()
+ }
+ }
+}
+
+// StringRGB returns a css RGB string representation of the color.
+func (c Color) StringRGB() string {
+ return fmt.Sprintf("rgb(%v,%v,%v)", c.R, c.G, c.B)
+}
+
+// StringRGBA returns a css RGBA string representation of the color.
+func (c Color) StringRGBA() string {
fa := float64(c.A) / float64(255)
return fmt.Sprintf("rgba(%v,%v,%v,%.1f)", c.R, c.G, c.B, fa)
}
diff --git a/chartdraw/drawing/raster_graphic_context.go b/chartdraw/drawing/raster_graphic_context.go
index f400bed..37ad521 100644
--- a/chartdraw/drawing/raster_graphic_context.go
+++ b/chartdraw/drawing/raster_graphic_context.go
@@ -14,15 +14,9 @@ import (
)
// NewRasterGraphicContext creates a new Graphic context from an image.
-func NewRasterGraphicContext(img draw.Image) (*RasterGraphicContext, error) {
- var painter Painter
- switch selectImage := img.(type) {
- case *image.RGBA:
- painter = raster.NewRGBAPainter(selectImage)
- default:
- return nil, errors.New("NewRasterGraphicContext() :: invalid image type")
- }
- return NewRasterGraphicContextWithPainter(img, painter), nil
+func NewRasterGraphicContext(img *image.RGBA) *RasterGraphicContext {
+ painter := raster.NewRGBAPainter(img)
+ return NewRasterGraphicContextWithPainter(img, painter)
}
// NewRasterGraphicContextWithPainter creates a new Graphic context from an image and a Painter (see Freetype-go)
diff --git a/chartdraw/examples/text_rotation/main.go b/chartdraw/examples/text_rotation/main.go
index 8a37aa7..e070bc6 100644
--- a/chartdraw/examples/text_rotation/main.go
+++ b/chartdraw/examples/text_rotation/main.go
@@ -11,7 +11,7 @@ import (
func main() {
f := chartdraw.GetDefaultFont()
- r, _ := chartdraw.PNG(1024, 1024)
+ r := chartdraw.PNG(1024, 1024)
chartdraw.Draw.Text(r, "Test", 64, 64, chartdraw.Style{
FontStyle: chartdraw.FontStyle{
diff --git a/chartdraw/pie_chart.go b/chartdraw/pie_chart.go
index fe855fd..e07d9ee 100644
--- a/chartdraw/pie_chart.go
+++ b/chartdraw/pie_chart.go
@@ -71,10 +71,7 @@ func (pc PieChart) Render(rp RendererProvider, w io.Writer) error {
return errors.New("please provide at least one value")
}
- r, err := rp(pc.GetWidth(), pc.GetHeight())
- if err != nil {
- return err
- }
+ r := rp(pc.GetWidth(), pc.GetHeight())
if pc.Font == nil {
pc.defaultFont = GetDefaultFont()
diff --git a/chartdraw/raster_renderer.go b/chartdraw/raster_renderer.go
index 86f061f..0dc549b 100644
--- a/chartdraw/raster_renderer.go
+++ b/chartdraw/raster_renderer.go
@@ -12,16 +12,13 @@ import (
)
// PNG returns a new png/raster renderer.
-func PNG(width, height int) (Renderer, error) {
+func PNG(width, height int) Renderer {
i := image.NewRGBA(image.Rect(0, 0, width, height))
- gc, err := drawing.NewRasterGraphicContext(i)
- if err == nil {
- return &rasterRenderer{
- i: i,
- gc: gc,
- }, nil
+ gc := drawing.NewRasterGraphicContext(i)
+ return &rasterRenderer{
+ i: i,
+ gc: gc,
}
- return nil, err
}
// rasterRenderer renders chart commands to a bitmap.
@@ -153,6 +150,9 @@ func (rr *rasterRenderer) SetFontColor(c drawing.Color) {
// Text implements the interface method.
func (rr *rasterRenderer) Text(body string, x, y int) {
+ if body == "" {
+ return
+ }
xf, yf := rr.getCoords(x, y)
rr.gc.SetFont(rr.s.Font)
rr.gc.SetFontSize(rr.s.FontSize)
diff --git a/chartdraw/renderer_provider.go b/chartdraw/renderer_provider.go
index 2290ead..dd75974 100644
--- a/chartdraw/renderer_provider.go
+++ b/chartdraw/renderer_provider.go
@@ -1,4 +1,4 @@
package chartdraw
// RendererProvider is a function that returns a renderer.
-type RendererProvider func(int, int) (Renderer, error)
+type RendererProvider func(int, int) Renderer
diff --git a/chartdraw/stacked_bar_chart.go b/chartdraw/stacked_bar_chart.go
index f6acafc..e064521 100644
--- a/chartdraw/stacked_bar_chart.go
+++ b/chartdraw/stacked_bar_chart.go
@@ -101,10 +101,7 @@ func (sbc StackedBarChart) Render(rp RendererProvider, w io.Writer) error {
return errors.New("please provide at least one bar")
}
- r, err := rp(sbc.GetWidth(), sbc.GetHeight())
- if err != nil {
- return err
- }
+ r := rp(sbc.GetWidth(), sbc.GetHeight())
if sbc.Font == nil {
sbc.defaultFont = GetDefaultFont()
diff --git a/chartdraw/style.go b/chartdraw/style.go
index 9049d63..409f55e 100644
--- a/chartdraw/style.go
+++ b/chartdraw/style.go
@@ -385,9 +385,7 @@ func (s Style) WriteToRenderer(r Renderer) {
r.SetStrokeWidth(s.GetStrokeWidth())
r.SetStrokeDashArray(s.GetStrokeDashArray())
r.SetFillColor(s.GetFillColor())
- r.SetFont(s.GetFont())
- r.SetFontColor(s.GetFontColor())
- r.SetFontSize(s.GetFontSize())
+ s.FontStyle.WriteTextOptionsToRenderer(r)
r.ClearTextRotation()
if s.GetTextRotationDegrees() != 0 {
diff --git a/chartdraw/text_test.go b/chartdraw/text_test.go
index 4919e8b..30ed6f9 100644
--- a/chartdraw/text_test.go
+++ b/chartdraw/text_test.go
@@ -10,8 +10,7 @@ import (
func TestTextWrapWord(t *testing.T) {
t.Parallel()
- r, err := PNG(1024, 1024)
- require.NoError(t, err)
+ r := PNG(1024, 1024)
basicTextStyle := Style{FontStyle: FontStyle{Font: GetDefaultFont(), FontSize: 24}}
@@ -44,8 +43,7 @@ func TestTextWrapWord(t *testing.T) {
func TestTextWrapRune(t *testing.T) {
t.Parallel()
- r, err := PNG(1024, 1024)
- require.NoError(t, err)
+ r := PNG(1024, 1024)
basicTextStyle := Style{FontStyle: FontStyle{Font: GetDefaultFont(), FontSize: 24}}
diff --git a/chartdraw/tick_test.go b/chartdraw/tick_test.go
index 0e59ae0..851bbb1 100644
--- a/chartdraw/tick_test.go
+++ b/chartdraw/tick_test.go
@@ -10,8 +10,7 @@ import (
func TestGenerateContinuousTicks(t *testing.T) {
t.Parallel()
- r, err := PNG(1024, 1024)
- require.NoError(t, err)
+ r := PNG(1024, 1024)
r.SetFont(GetDefaultFont())
ra := &ContinuousRange{
@@ -32,8 +31,7 @@ func TestGenerateContinuousTicks(t *testing.T) {
func TestGenerateContinuousTicksDescending(t *testing.T) {
t.Parallel()
- r, err := PNG(1024, 1024)
- require.NoError(t, err)
+ r := PNG(1024, 1024)
r.SetFont(GetDefaultFont())
ra := &ContinuousRange{
diff --git a/chartdraw/vector_renderer.go b/chartdraw/vector_renderer.go
index cf19e95..58f028d 100644
--- a/chartdraw/vector_renderer.go
+++ b/chartdraw/vector_renderer.go
@@ -5,6 +5,7 @@ import (
"fmt"
"io"
"math"
+ "strconv"
"strings"
"golang.org/x/image/font"
@@ -15,7 +16,7 @@ import (
)
// SVG returns a new png/raster renderer.
-func SVG(width, height int) (Renderer, error) {
+func SVG(width, height int) Renderer {
buffer := bytes.NewBuffer([]byte{})
canvas := newCanvas(buffer)
canvas.Start(width, height)
@@ -25,13 +26,13 @@ func SVG(width, height int) (Renderer, error) {
s: &Style{},
p: []string{},
dpi: DefaultDPI,
- }, nil
+ }
}
// SVGWithCSS returns a new png/raster renderer with attached custom CSS
// The optional nonce argument sets a CSP nonce.
-func SVGWithCSS(css string, nonce string) func(width, height int) (Renderer, error) {
- return func(width, height int) (Renderer, error) {
+func SVGWithCSS(css string, nonce string) func(width, height int) Renderer {
+ return func(width, height int) Renderer {
buffer := bytes.NewBuffer([]byte{})
canvas := newCanvas(buffer)
canvas.css = css
@@ -43,7 +44,7 @@ func SVGWithCSS(css string, nonce string) func(width, height int) (Renderer, err
s: &Style{},
p: []string{},
dpi: DefaultDPI,
- }, nil
+ }
}
}
@@ -207,6 +208,7 @@ func (vr *vectorRenderer) MeasureText(body string) (box Box) {
box.Right = w
box.Bottom = int(drawing.PointsToPixels(vr.dpi, vr.s.FontSize))
+ box.IsSet = true
if vr.c.textTheta == nil {
return
}
@@ -266,24 +268,30 @@ func (c *canvas) Start(width, height int) {
}
func (c *canvas) Path(d string, style Style) {
+ if d == "" {
+ return
+ }
var strokeDashArrayProperty string
if len(style.StrokeDashArray) > 0 {
strokeDashArrayProperty = c.getStrokeDashArray(style)
}
- _, _ = c.w.Write([]byte(fmt.Sprintf(``, strokeDashArrayProperty, d, c.styleAsSVG(style))))
+ _, _ = c.w.Write([]byte(fmt.Sprintf(``, strokeDashArrayProperty, d, c.styleAsSVG(style, false))))
}
func (c *canvas) Text(x, y int, body string, style Style) {
+ if body == "" {
+ return
+ }
if c.textTheta == nil {
- _, _ = c.w.Write([]byte(fmt.Sprintf(`%s`, x, y, c.styleAsSVG(style), body)))
+ _, _ = c.w.Write([]byte(fmt.Sprintf(`%s`, x, y, c.styleAsSVG(style, true), body)))
} else {
transform := fmt.Sprintf(` transform="rotate(%0.2f,%d,%d)"`, RadiansToDegrees(*c.textTheta), x, y)
- _, _ = c.w.Write([]byte(fmt.Sprintf(`%s`, x, y, c.styleAsSVG(style), transform, body)))
+ _, _ = c.w.Write([]byte(fmt.Sprintf(`%s`, x, y, c.styleAsSVG(style, true), transform, body)))
}
}
func (c *canvas) Circle(x, y, r int, style Style) {
- _, _ = c.w.Write([]byte(fmt.Sprintf(``, x, y, r, c.styleAsSVG(style))))
+ _, _ = c.w.Write([]byte(fmt.Sprintf(``, x, y, r, c.styleAsSVG(style, false))))
}
func (c *canvas) End() {
@@ -315,7 +323,7 @@ func (c *canvas) getFontFace(s Style) string {
}
// styleAsSVG returns the style as a svg style or class string.
-func (c *canvas) styleAsSVG(s Style) string {
+func (c *canvas) styleAsSVG(s Style, applyText bool) string {
sw := s.StrokeWidth
sc := s.StrokeColor
fc := s.FillColor
@@ -331,7 +339,7 @@ func (c *canvas) styleAsSVG(s Style) string {
if !fc.IsZero() {
classes = append(classes, "fill")
}
- if fs != 0 || s.Font != nil {
+ if applyText && (fs != 0 || s.Font != nil) {
classes = append(classes, "text")
}
@@ -340,19 +348,14 @@ func (c *canvas) styleAsSVG(s Style) string {
var pieces []string
- if sw != 0 {
- pieces = append(pieces, "stroke-width:"+fmt.Sprintf("%d", int(sw)))
- } else {
- pieces = append(pieces, "stroke-width:0")
- }
-
- if !sc.IsTransparent() {
+ if sw != 0 && !sc.IsTransparent() {
+ pieces = append(pieces, "stroke-width:"+formatFloatMinimized(sw))
pieces = append(pieces, "stroke:"+sc.String())
} else {
pieces = append(pieces, "stroke:none")
}
- if !fnc.IsTransparent() {
+ if applyText && !fnc.IsTransparent() {
pieces = append(pieces, "fill:"+fnc.String())
} else if !fc.IsTransparent() {
pieces = append(pieces, "fill:"+fc.String())
@@ -360,12 +363,29 @@ func (c *canvas) styleAsSVG(s Style) string {
pieces = append(pieces, "fill:none")
}
- if fs != 0 {
- pieces = append(pieces, "font-size:"+fmt.Sprintf("%.1fpx", drawing.PointsToPixels(c.dpi, fs)))
+ if applyText {
+ if fs != 0 {
+ pieces = append(pieces, "font-size:"+formatFloatMinimized(drawing.PointsToPixels(c.dpi, fs))+"px")
+ }
+ if s.Font != nil {
+ pieces = append(pieces, c.getFontFace(s))
+ }
}
- if s.Font != nil {
- pieces = append(pieces, c.getFontFace(s))
+ if len(pieces) == 0 {
+ return ""
}
+
return fmt.Sprintf("style=\"%s\"", strings.Join(pieces, ";"))
}
+
+// formatFloatNoTrailingZero formats a float without trailing zeros, so it is as small as possible.
+func formatFloatMinimized(val float64) string {
+ if val == float64(int(val)) {
+ return strconv.Itoa(int(val))
+ }
+ str := fmt.Sprintf("%.1f", val) // e.g. "1.20"
+ str = strings.TrimRight(str, "0") // e.g. "1.2"
+ str = strings.TrimRight(str, ".") // a rounding condition where an int is acceptable
+ return str
+}
diff --git a/chartdraw/vector_renderer_test.go b/chartdraw/vector_renderer_test.go
index 2f1be7a..3405b1f 100644
--- a/chartdraw/vector_renderer_test.go
+++ b/chartdraw/vector_renderer_test.go
@@ -15,8 +15,7 @@ import (
func TestVectorRendererPath(t *testing.T) {
t.Parallel()
- vr, err := SVG(100, 100)
- require.NoError(t, err)
+ vr := SVG(100, 100)
typed, isTyped := vr.(*vectorRenderer)
assert.True(t, isTyped)
@@ -38,8 +37,7 @@ func TestVectorRendererPath(t *testing.T) {
func TestVectorRendererMeasureText(t *testing.T) {
t.Parallel()
- vr, err := SVG(100, 100)
- require.NoError(t, err)
+ vr := SVG(100, 100)
vr.SetDPI(DefaultDPI)
vr.SetFont(GetDefaultFont())
@@ -60,18 +58,30 @@ func TestCanvasStyleSVG(t *testing.T) {
FontStyle: FontStyle{
FontColor: drawing.ColorWhite,
Font: GetDefaultFont(),
+ FontSize: 12,
},
Padding: DefaultBackgroundPadding,
}
canvas := &canvas{dpi: DefaultDPI}
- svgString := canvas.styleAsSVG(set)
+ svgString := canvas.styleAsSVG(set, false)
assert.NotEmpty(t, svgString)
assert.True(t, strings.HasPrefix(svgString, "style=\""))
- assert.True(t, strings.Contains(svgString, "stroke:rgba(255,255,255,1.0)"))
+ assert.True(t, strings.Contains(svgString, "stroke:white"))
assert.True(t, strings.Contains(svgString, "stroke-width:5"))
- assert.True(t, strings.Contains(svgString, "fill:rgba(255,255,255,1.0)"))
+ assert.True(t, strings.Contains(svgString, "fill:white"))
+ assert.False(t, strings.Contains(svgString, "font-size"))
+ assert.False(t, strings.Contains(svgString, "font-family"))
+ assert.True(t, strings.HasSuffix(svgString, "\""))
+
+ svgString = canvas.styleAsSVG(set, true)
+ assert.True(t, strings.HasPrefix(svgString, "style=\""))
+ assert.True(t, strings.Contains(svgString, "stroke:white"))
+ assert.True(t, strings.Contains(svgString, "stroke-width:5"))
+ assert.True(t, strings.Contains(svgString, "fill:white"))
+ assert.True(t, strings.Contains(svgString, "font-size"))
+ assert.True(t, strings.Contains(svgString, "font-family"))
assert.True(t, strings.HasSuffix(svgString, "\""))
}
@@ -84,7 +94,7 @@ func TestCanvasClassSVG(t *testing.T) {
canvas := &canvas{dpi: DefaultDPI}
- assert.Equal(t, "class=\"test-class\"", canvas.styleAsSVG(set))
+ assert.Equal(t, "class=\"test-class\"", canvas.styleAsSVG(set, false))
}
func TestCanvasCustomInlineStylesheet(t *testing.T) {
diff --git a/chartdraw/xaxis_test.go b/chartdraw/xaxis_test.go
index 4f6e331..ef78a72 100644
--- a/chartdraw/xaxis_test.go
+++ b/chartdraw/xaxis_test.go
@@ -4,14 +4,12 @@ import (
"testing"
"github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
)
func TestXAxisGetTicks(t *testing.T) {
t.Parallel()
- r, err := PNG(1024, 1024)
- require.NoError(t, err)
+ r := PNG(1024, 1024)
xa := XAxis{}
xr := &ContinuousRange{Min: 10, Max: 100, Domain: 1024}
@@ -29,8 +27,7 @@ func TestXAxisGetTicks(t *testing.T) {
func TestXAxisGetTicksWithUserDefaults(t *testing.T) {
t.Parallel()
- r, err := PNG(1024, 1024)
- require.NoError(t, err)
+ r := PNG(1024, 1024)
xa := XAxis{
Ticks: []Tick{{Value: 1.0, Label: "1.0"}},
@@ -56,8 +53,7 @@ func TestXAxisMeasure(t *testing.T) {
FontSize: 10.0,
},
}
- r, err := PNG(100, 100)
- require.NoError(t, err)
+ r := PNG(100, 100)
ticks := []Tick{{Value: 1.0, Label: "1.0"}, {Value: 2.0, Label: "2.0"}, {Value: 3.0, Label: "3.0"}}
xa := XAxis{}
xab := xa.Measure(r, NewBox(0, 0, 100, 100), &ContinuousRange{Min: 1.0, Max: 3.0, Domain: 100}, style, ticks)
diff --git a/chartdraw/yaxis_test.go b/chartdraw/yaxis_test.go
index 398a99c..ef54abb 100644
--- a/chartdraw/yaxis_test.go
+++ b/chartdraw/yaxis_test.go
@@ -4,14 +4,12 @@ import (
"testing"
"github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
)
func TestYAxisGetTicks(t *testing.T) {
t.Parallel()
- r, err := PNG(1024, 1024)
- require.NoError(t, err)
+ r := PNG(1024, 1024)
ya := YAxis{}
yr := &ContinuousRange{Min: 10, Max: 100, Domain: 1024}
@@ -29,8 +27,7 @@ func TestYAxisGetTicks(t *testing.T) {
func TestYAxisGetTicksWithUserDefaults(t *testing.T) {
t.Parallel()
- r, err := PNG(1024, 1024)
- require.NoError(t, err)
+ r := PNG(1024, 1024)
ya := YAxis{
Ticks: []Tick{{Value: 1.0, Label: "1.0"}},
@@ -56,8 +53,7 @@ func TestYAxisMeasure(t *testing.T) {
FontSize: 10.0,
},
}
- r, err := PNG(100, 100)
- require.NoError(t, err)
+ r := PNG(100, 100)
ticks := []Tick{{Value: 1.0, Label: "1.0"}, {Value: 2.0, Label: "2.0"}, {Value: 3.0, Label: "3.0"}}
ya := YAxis{}
yab := ya.Measure(r, NewBox(0, 0, 100, 100), &ContinuousRange{Min: 1.0, Max: 3.0, Domain: 100}, style, ticks)
@@ -74,8 +70,7 @@ func TestYAxisSecondaryMeasure(t *testing.T) {
FontSize: 10.0,
},
}
- r, err := PNG(100, 100)
- require.NoError(t, err)
+ r := PNG(100, 100)
ticks := []Tick{{Value: 1.0, Label: "1.0"}, {Value: 2.0, Label: "2.0"}, {Value: 3.0, Label: "3.0"}}
ya := YAxis{AxisType: YAxisSecondary}
yab := ya.Measure(r, NewBox(0, 0, 100, 100), &ContinuousRange{Min: 1.0, Max: 3.0, Domain: 100}, style, ticks)
diff --git a/charts.go b/charts.go
index 1708db0..701a1e5 100644
--- a/charts.go
+++ b/charts.go
@@ -17,16 +17,13 @@ const defaultYAxisLabelCountLow = 3
var defaultChartWidth = 600
var defaultChartHeight = 400
+var defaultPadding = chartdraw.NewBox(20, 20, 20, 20)
-// SetDefaultWidth sets default width of chart
-func SetDefaultWidth(width int) {
+// SetDefaultChartDimensions sets default width and height of charts if not otherwise specified in their configuration.
+func SetDefaultChartDimensions(width, height int) {
if width > 0 {
defaultChartWidth = width
}
-}
-
-// SetDefaultHeight sets default height of chart
-func SetDefaultHeight(height int) {
if height > 0 {
defaultChartHeight = height
}
@@ -46,7 +43,7 @@ func defaultYAxisLabelCount(span float64, decimalData bool) int {
return int(result)
}
-type Renderer interface {
+type renderer interface {
Render() (Box, error)
}
@@ -71,25 +68,27 @@ func (rh *renderHandler) Do() error {
return nil
}
-type defaultRenderOption struct { // TODO - change names to be lower case for consistency
- // Theme specifies the colors used for the chart.
- Theme ColorPalette
- // Padding specifies the padding of chart.
- Padding Box
- // SeriesList provides the data series.
- SeriesList SeriesList
- // XAxis are options for the x-axis.
- XAxis XAxisOption
- // YAxis are options for the y-axis (at most two).
- YAxis []YAxisOption
- // Title are options for rendering the title.
- Title TitleOption
- // Legend are options for the data legend.
- Legend LegendOption
+type defaultRenderOption struct {
+ // theme specifies the colors used for the chart.
+ theme ColorPalette
+ // padding specifies the padding of chart.
+ padding Box
+ // seriesList provides the data series.
+ seriesList SeriesList
+ // xAxis are options for the x-axis.
+ xAxis *XAxisOption
+ // yAxis are options for the y-axis (at most two).
+ yAxis []YAxisOption
+ // title are options for rendering the title.
+ title TitleOption
+ // legend are options for the data legend.
+ legend *LegendOption
// backgroundIsFilled is true if the background is filled.
backgroundIsFilled bool
- // axisReversed is true if the x y axis is reversed.
+ // axisReversed is true if the x y-axis is reversed.
axisReversed bool
+ // valueFormatter to format numeric values into labels.
+ valueFormatter ValueFormatter
}
type defaultRenderResult struct {
@@ -99,25 +98,44 @@ type defaultRenderResult struct {
}
func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, error) {
- fillThemeDefaults(getPreferredTheme(opt.Theme), &opt.Title, &opt.Legend, &opt.XAxis)
+ fillThemeDefaults(getPreferredTheme(opt.theme, p.theme), &opt.title, opt.legend, opt.xAxis)
- seriesList := opt.SeriesList
+ seriesList := opt.seriesList
seriesList.init()
if !opt.backgroundIsFilled {
- p.SetBackground(p.Width(), p.Height(), opt.Theme.GetBackgroundColor())
+ p.SetBackground(p.Width(), p.Height(), opt.theme.GetBackgroundColor())
+ }
+ if !opt.padding.IsZero() {
+ p = p.Child(PainterPaddingOption(opt.padding))
}
- if !opt.Padding.IsZero() {
- p = p.Child(PainterPaddingOption(opt.Padding))
+ // association between legend and series name
+ if len(opt.legend.Data) == 0 {
+ opt.legend.Data = opt.seriesList.Names()
+ } else {
+ seriesCount := len(opt.seriesList)
+ for index, name := range opt.legend.Data {
+ if index < seriesCount && len(opt.seriesList[index].Name) == 0 {
+ opt.seriesList[index].Name = name
+ }
+ }
+ nameIndexDict := map[string]int{}
+ for index, name := range opt.legend.Data {
+ nameIndexDict[name] = index
+ }
+ // ensure order of series is consistent with legend
+ sort.Slice(opt.seriesList, func(i, j int) bool {
+ return nameIndexDict[opt.seriesList[i].Name] < nameIndexDict[opt.seriesList[j].Name]
+ })
}
const legendTitlePadding = 15
legendTopSpacing := 0
- legendResult, err := NewLegendPainter(p, opt.Legend).Render()
+ legendResult, err := newLegendPainter(p, *opt.legend).Render()
if err != nil {
return nil, err
}
- if !legendResult.IsZero() && !opt.Legend.Vertical && !flagIs(true, opt.Legend.OverlayChart) {
+ if !legendResult.IsZero() && !flagIs(true, opt.legend.Vertical) && !flagIs(true, opt.legend.OverlayChart) {
legendHeight := legendResult.Height()
if legendResult.Bottom < p.Height()/2 {
// horizontal legend at the top, set the spacing based on the height
@@ -131,7 +149,7 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e
}
}
- titleBox, err := NewTitlePainter(p, opt.Title).Render()
+ titleBox, err := newTitlePainter(p, opt.title).Render()
if err != nil {
return nil, err
}
@@ -162,7 +180,7 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e
}
axisIndexList := make([]int, 0)
- for _, series := range opt.SeriesList {
+ for _, series := range opt.seriesList {
if containsInt(axisIndexList, series.YAxisIndex) {
continue
}
@@ -173,20 +191,20 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e
rangeWidthLeft := 0
rangeWidthRight := 0
- sort.Sort(sort.Reverse(sort.IntSlice(axisIndexList)))
+ reverseIntSlice(axisIndexList)
// calculate the axis range
for _, index := range axisIndexList {
yAxisOption := YAxisOption{}
- if len(opt.YAxis) > index {
- yAxisOption = opt.YAxis[index]
+ if len(opt.yAxis) > index {
+ yAxisOption = opt.yAxis[index]
}
minPadRange, maxPadRange := 1.0, 1.0
if yAxisOption.RangeValuePaddingScale != nil {
minPadRange = *yAxisOption.RangeValuePaddingScale
maxPadRange = *yAxisOption.RangeValuePaddingScale
}
- min, max := opt.SeriesList.GetMinMax(index)
+ min, max := opt.seriesList.GetMinMax(index)
decimalData := min != math.Floor(min) || (max-min) != math.Floor(max-min)
if yAxisOption.Min != nil && *yAxisOption.Min < min {
min = *yAxisOption.Min
@@ -199,8 +217,8 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e
// Label counts and y-axis padding are linked together to produce a user-friendly graph.
// First when considering padding we want to prefer a zero axis start if reasonable, and add a slight
- // padding to the max so there is a little space at the top of the graph. In addition, we want to pick
- // a max value that will result in round intervals on the axis. These details are in range.go.
+ // padding to the max so there is a little space at the top of the graph. In addition, we want to pick
+ // a max value that will result in round intervals on the axis. These details are in range.go.
// But in order to produce round intervals we need to have an idea of how many intervals there are.
// In addition, if the user specified a `Unit` value we may need to adjust our label count calculation
// based on the padded range.
@@ -232,28 +250,23 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e
yAxisOption.LabelCount = labelCount
}
labelCount = chartdraw.MaxInt(labelCount+yAxisOption.LabelCountAdjustment, 2)
- r := axisRange{
- p: p,
- divideCount: labelCount,
- min: min,
- max: max,
- size: rangeHeight,
- }
+ r := newRange(p, getPreferredValueFormatter(yAxisOption.ValueFormatter, opt.valueFormatter),
+ rangeHeight, labelCount, min, max, 0, 0)
result.axisRanges[index] = r
if yAxisOption.Theme == nil {
- yAxisOption.Theme = opt.Theme
+ yAxisOption.Theme = opt.theme
}
if !opt.axisReversed {
yAxisOption.Data = r.Values()
} else {
yAxisOption.isCategoryAxis = true
- // we need to update the range labels or the bars wont be aligned to the Y axis
+ // we need to update the range labels or the bars won't be aligned to the Y axis
r.divideCount = len(seriesList[0].Data)
result.axisRanges[index] = r
// since the x-axis is the value part, it's label is calculated and processed separately
- opt.XAxis.Data = r.Values()
- opt.XAxis.isValueAxis = true
+ opt.xAxis.Data = r.Values()
+ opt.xAxis.isValueAxis = true
}
reverseStringSlice(yAxisOption.Data)
child := p.Child(PainterPaddingOption(Box{
@@ -263,9 +276,9 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e
}))
var yAxis *axisPainter
if index == 0 {
- yAxis = NewLeftYAxis(child, yAxisOption)
+ yAxis = newLeftYAxis(child, yAxisOption)
} else {
- yAxis = NewRightYAxis(child, yAxisOption)
+ yAxis = newRightYAxis(child, yAxisOption)
}
if yAxisBox, err := yAxis.Render(); err != nil {
return nil, err
@@ -276,11 +289,11 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e
}
}
- xAxis := NewBottomXAxis(p.Child(PainterPaddingOption(Box{
+ xAxis := newBottomXAxis(p.Child(PainterPaddingOption(Box{
Left: rangeWidthLeft,
Right: rangeWidthRight,
IsSet: true,
- })), opt.XAxis)
+ })), *opt.xAxis)
if _, err := xAxis.Render(); err != nil {
return nil, err
}
@@ -294,7 +307,7 @@ func defaultRender(p *Painter, opt defaultRenderOption) (*defaultRenderResult, e
return &result, nil
}
-func doRender(renderers ...Renderer) error {
+func doRender(renderers ...renderer) error {
for _, r := range renderers {
if _, err := r.Render(); err != nil {
return err
@@ -313,21 +326,14 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) {
isChild := opt.parent != nil
if !isChild {
- p, err := NewPainter(PainterOptions{
+ opt.parent = NewPainter(PainterOptions{
OutputFormat: opt.OutputFormat,
Width: opt.Width,
Height: opt.Height,
Font: opt.Font,
})
- if err != nil {
- return nil, err
- }
- opt.parent = p
}
p := opt.parent
- if opt.ValueFormatter != nil {
- p.valueFormatter = opt.ValueFormatter
- }
if !opt.Box.IsZero() {
p = p.Child(PainterBoxOption(opt.Box))
}
@@ -357,29 +363,30 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) {
axisReversed := len(horizontalBarSeriesList) != 0
renderOpt := defaultRenderOption{
- Theme: opt.Theme,
- Padding: opt.Padding,
- SeriesList: opt.SeriesList,
- XAxis: opt.XAxis,
- YAxis: opt.YAxis,
- Title: opt.Title,
- Legend: opt.Legend,
- axisReversed: axisReversed,
+ theme: opt.Theme,
+ padding: opt.Padding,
+ seriesList: opt.SeriesList,
+ xAxis: &opt.XAxis,
+ yAxis: opt.YAxis,
+ title: opt.Title,
+ legend: &opt.Legend,
+ axisReversed: axisReversed,
+ valueFormatter: opt.ValueFormatter,
// the background color has been set
backgroundIsFilled: true,
}
if len(pieSeriesList) != 0 ||
len(radarSeriesList) != 0 ||
len(funnelSeriesList) != 0 {
- renderOpt.XAxis.Show = False()
- renderOpt.YAxis = []YAxisOption{
+ renderOpt.xAxis.Show = False()
+ renderOpt.yAxis = []YAxisOption{
{
Show: False(),
},
}
}
if len(horizontalBarSeriesList) != 0 {
- renderOpt.YAxis[0].Unit = 1
+ renderOpt.yAxis[0].Unit = 1
}
renderResult, err := defaultRender(p, renderOpt)
@@ -392,7 +399,7 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) {
// bar chart
if len(barSeriesList) != 0 {
handler.Add(func() error {
- _, err := NewBarChart(p, BarChartOption{
+ _, err := newBarChart(p, BarChartOption{
Theme: opt.Theme,
Font: opt.Font,
XAxis: opt.XAxis,
@@ -405,7 +412,7 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) {
// horizontal bar chart
if len(horizontalBarSeriesList) != 0 {
handler.Add(func() error {
- _, err := NewHorizontalBarChart(p, HorizontalBarChartOption{
+ _, err := newHorizontalBarChart(p, HorizontalBarChartOption{
Theme: opt.Theme,
Font: opt.Font,
BarHeight: opt.BarHeight,
@@ -418,7 +425,7 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) {
// pie chart
if len(pieSeriesList) != 0 {
handler.Add(func() error {
- _, err := NewPieChart(p, PieChartOption{
+ _, err := newPieChart(p, PieChartOption{
Theme: opt.Theme,
Font: opt.Font,
}).render(renderResult, pieSeriesList)
@@ -429,14 +436,14 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) {
// line chart
if len(lineSeriesList) != 0 {
handler.Add(func() error {
- _, err := NewLineChart(p, LineChartOption{
- Theme: opt.Theme,
- Font: opt.Font,
- XAxis: opt.XAxis,
- SymbolShow: opt.SymbolShow,
- StrokeWidth: opt.LineStrokeWidth,
- FillArea: opt.FillArea,
- FillOpacity: opt.FillOpacity,
+ _, err := newLineChart(p, LineChartOption{
+ Theme: opt.Theme,
+ Font: opt.Font,
+ XAxis: opt.XAxis,
+ SymbolShow: opt.SymbolShow,
+ LineStrokeWidth: opt.LineStrokeWidth,
+ FillArea: opt.FillArea,
+ FillOpacity: opt.FillOpacity,
}).render(renderResult, lineSeriesList)
return err
})
@@ -445,7 +452,7 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) {
// radar chart
if len(radarSeriesList) != 0 {
handler.Add(func() error {
- _, err := NewRadarChart(p, RadarChartOption{
+ _, err := newRadarChart(p, RadarChartOption{
Theme: opt.Theme,
Font: opt.Font,
RadarIndicators: opt.RadarIndicators,
@@ -457,7 +464,7 @@ func Render(opt ChartOption, opts ...OptionFunc) (*Painter, error) {
// funnel chart
if len(funnelSeriesList) != 0 {
handler.Add(func() error {
- _, err := NewFunnelChart(p, FunnelChartOption{
+ _, err := newFunnelChart(p, FunnelChartOption{
Theme: opt.Theme,
Font: opt.Font,
}).render(renderResult, funnelSeriesList)
diff --git a/charts_test.go b/charts_test.go
index b543540..e98a03e 100644
--- a/charts_test.go
+++ b/charts_test.go
@@ -16,10 +16,7 @@ func BenchmarkMultiChartPNGRender(b *testing.B) {
Top: "-90",
},
Data: []string{
- "Milk Tea",
- "Matcha Latte",
- "Cheese Cocoa",
- "Walnut Brownie",
+ "Milk Tea", "Matcha Latte", "Cheese Cocoa", "Walnut Brownie",
},
},
Padding: chartdraw.Box{
@@ -30,12 +27,7 @@ func BenchmarkMultiChartPNGRender(b *testing.B) {
},
XAxis: XAxisOption{
Data: []string{
- "2012",
- "2013",
- "2014",
- "2015",
- "2016",
- "2017",
+ "2012", "2013", "2014", "2015", "2016", "2017",
},
},
YAxis: []YAxisOption{
@@ -45,49 +37,21 @@ func BenchmarkMultiChartPNGRender(b *testing.B) {
Max: FloatPointer(90),
},
},
- SeriesList: []Series{
- NewSeriesFromValues([]float64{
- 56.5,
- 82.1,
- 88.7,
- 70.1,
- 53.4,
- 85.1,
+ SeriesList: append(
+ NewSeriesListLine([][]float64{
+ {56.5, 82.1, 88.7, 70.1, 53.4, 85.1},
+ {51.1, 51.4, 55.1, 53.3, 73.8, 68.7},
}),
- NewSeriesFromValues([]float64{
- 51.1,
- 51.4,
- 55.1,
- 53.3,
- 73.8,
- 68.7,
- }),
- NewSeriesFromValues([]float64{
- 40.1,
- 62.2,
- 69.5,
- 36.4,
- 45.2,
- 32.5,
- }, ChartTypeBar),
- NewSeriesFromValues([]float64{
- 25.2,
- 37.1,
- 41.2,
- 18,
- 33.9,
- 49.1,
- }, ChartTypeBar),
- },
+ NewSeriesListBar([][]float64{
+ {40.1, 62.2, 69.5, 36.4, 45.2, 32.5},
+ {25.2, 37.1, 41.2, 18, 33.9, 49.1},
+ })...),
Children: []ChartOption{
{
Legend: LegendOption{
Show: False(),
Data: []string{
- "Milk Tea",
- "Matcha Latte",
- "Cheese Cocoa",
- "Walnut Brownie",
+ "Milk Tea", "Matcha Latte", "Cheese Cocoa", "Walnut Brownie",
},
},
Box: chartdraw.Box{
@@ -96,15 +60,9 @@ func BenchmarkMultiChartPNGRender(b *testing.B) {
Right: 500,
Bottom: 120,
},
- SeriesList: NewPieSeriesList([]float64{
- 435.9,
- 354.3,
- 285.9,
- 204.5,
+ SeriesList: NewSeriesListPie([]float64{
+ 435.9, 354.3, 285.9, 204.5,
}, PieSeriesOption{
- Label: SeriesLabel{
- Show: true,
- },
Radius: "35%",
}),
},
@@ -129,10 +87,7 @@ func BenchmarkMultiChartSVGRender(b *testing.B) {
Top: "-90",
},
Data: []string{
- "Milk Tea",
- "Matcha Latte",
- "Cheese Cocoa",
- "Walnut Brownie",
+ "Milk Tea", "Matcha Latte", "Cheese Cocoa", "Walnut Brownie",
},
},
Padding: chartdraw.Box{
@@ -157,49 +112,21 @@ func BenchmarkMultiChartSVGRender(b *testing.B) {
Max: FloatPointer(90),
},
},
- SeriesList: []Series{
- NewSeriesFromValues([]float64{
- 56.5,
- 82.1,
- 88.7,
- 70.1,
- 53.4,
- 85.1,
+ SeriesList: append(
+ NewSeriesListLine([][]float64{
+ {56.5, 82.1, 88.7, 70.1, 53.4, 85.1},
+ {51.1, 51.4, 55.1, 53.3, 73.8, 68.7},
}),
- NewSeriesFromValues([]float64{
- 51.1,
- 51.4,
- 55.1,
- 53.3,
- 73.8,
- 68.7,
- }),
- NewSeriesFromValues([]float64{
- 40.1,
- 62.2,
- 69.5,
- 36.4,
- 45.2,
- 32.5,
- }, ChartTypeBar),
- NewSeriesFromValues([]float64{
- 25.2,
- 37.1,
- 41.2,
- 18,
- 33.9,
- 49.1,
- }, ChartTypeBar),
- },
+ NewSeriesListBar([][]float64{
+ {40.1, 62.2, 69.5, 36.4, 45.2, 32.5},
+ {25.2, 37.1, 41.2, 18, 33.9, 49.1},
+ })...),
Children: []ChartOption{
{
Legend: LegendOption{
Show: False(),
Data: []string{
- "Milk Tea",
- "Matcha Latte",
- "Cheese Cocoa",
- "Walnut Brownie",
+ "Milk Tea", "Matcha Latte", "Cheese Cocoa", "Walnut Brownie",
},
},
Box: chartdraw.Box{
@@ -208,15 +135,9 @@ func BenchmarkMultiChartSVGRender(b *testing.B) {
Right: 500,
Bottom: 120,
},
- SeriesList: NewPieSeriesList([]float64{
- 435.9,
- 354.3,
- 285.9,
- 204.5,
+ SeriesList: NewSeriesListPie([]float64{
+ 435.9, 354.3, 285.9, 204.5,
}, PieSeriesOption{
- Label: SeriesLabel{
- Show: true,
- },
Radius: "35%",
}),
},
diff --git a/echarts.go b/echarts.go
index b804944..823e6d7 100644
--- a/echarts.go
+++ b/echarts.go
@@ -47,17 +47,13 @@ func (value *EChartsSeriesDataValue) UnmarshalJSON(data []byte) error {
data = convertToArray(data)
return json.Unmarshal(data, &value.values)
}
+
func (value *EChartsSeriesDataValue) First() float64 {
if len(value.values) == 0 {
return 0
}
return value.values[0]
}
-func NewEChartsSeriesDataValue(values ...float64) EChartsSeriesDataValue {
- return EChartsSeriesDataValue{
- values: values,
- }
-}
type EChartsSeriesData struct {
Value EChartsSeriesDataValue `json:"value"`
@@ -275,14 +271,10 @@ func (esList EChartsSeriesList) ToSeriesList() SeriesList {
Type: item.Type,
Name: dataItem.Name,
Label: SeriesLabel{
- Show: true,
+ Show: True(),
},
Radius: item.Radius,
- Data: []SeriesData{
- {
- Value: dataItem.Value.First(),
- },
- },
+ Data: []float64{dataItem.Value.First()},
})
}
continue
@@ -293,25 +285,21 @@ func (esList EChartsSeriesList) ToSeriesList() SeriesList {
seriesList = append(seriesList, Series{
Name: dataItem.Name,
Type: item.Type,
- Data: NewSeriesDataFromValues(dataItem.Value.values),
- Max: item.Max,
- Min: item.Min,
+ Data: dataItem.Value.values,
Label: SeriesLabel{
FontStyle: FontStyle{
FontColor: ParseColor(item.Label.Color),
},
- Show: item.Label.Show,
+ Show: BoolPointer(item.Label.Show),
Distance: item.Label.Distance,
},
})
}
continue
}
- data := make([]SeriesData, len(item.Data))
+ data := make([]float64, len(item.Data))
for j, dataItem := range item.Data {
- data[j] = SeriesData{
- Value: dataItem.Value.First(),
- }
+ data[j] = dataItem.Value.First()
}
seriesList = append(seriesList, Series{
Type: item.Type,
@@ -321,7 +309,7 @@ func (esList EChartsSeriesList) ToSeriesList() SeriesList {
FontStyle: FontStyle{
FontColor: ParseColor(item.Label.Color),
},
- Show: item.Label.Show,
+ Show: BoolPointer(item.Label.Show),
Distance: item.Label.Distance,
},
Name: item.Name,
@@ -417,7 +405,7 @@ func (eo *EChartsOption) ToOption() ChartOption {
Top: string(eo.Legend.Top),
},
Align: eo.Legend.Align,
- Vertical: strings.EqualFold(eo.Legend.Orient, "vertical"),
+ Vertical: BoolPointer(strings.EqualFold(eo.Legend.Orient, "vertical")),
},
RadarIndicators: eo.Radar.Indicator,
Width: eo.Width,
diff --git a/echarts_test.go b/echarts_test.go
index fd11843..8ef9f7f 100644
--- a/echarts_test.go
+++ b/echarts_test.go
@@ -34,7 +34,7 @@ func TestEChartsSeriesDataValue(t *testing.T) {
assert.Equal(t, EChartsSeriesDataValue{
values: []float64{1, 2},
}, es)
- assert.Equal(t, NewEChartsSeriesDataValue(1, 2), es)
+ assert.Equal(t, EChartsSeriesDataValue{values: []float64{1, 2}}, es)
assert.Equal(t, 1.0, es.First())
}
@@ -525,5 +525,5 @@ func TestRenderEChartsToSVG(t *testing.T) {
]
}`)
require.NoError(t, err)
- assertEqualSVG(t, "", data)
+ assertEqualSVG(t, "", data)
}
diff --git a/examples/bar_chart-2/main.go b/examples/bar_chart-2/main.go
index 6302bcf..e7b8b12 100644
--- a/examples/bar_chart-2/main.go
+++ b/examples/bar_chart-2/main.go
@@ -27,8 +27,7 @@ func main() {
{2.6, 5.9, 9.0, 26.4, 28.7, 70.7, 175.6, 182.2, 48.7, 18.8, 6.0, 2.3},
}
- opt := charts.BarChartOption{}
- opt.SeriesList = charts.NewSeriesListDataFromValues(values, charts.ChartTypeBar)
+ opt := charts.NewBarChartOptionWithData(values)
opt.XAxis.Data = []string{
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
}
@@ -49,15 +48,12 @@ func main() {
charts.SeriesMarkDataTypeMin,
)
- p, err := charts.NewPainter(charts.PainterOptions{
+ p := charts.NewPainter(charts.PainterOptions{
OutputFormat: charts.ChartOutputPNG,
Width: 600,
Height: 400,
})
- if err != nil {
- panic(err)
- }
- if _, err = charts.NewBarChart(p, opt).Render(); err != nil {
+ if err := p.BarChart(opt); err != nil {
panic(err)
}
diff --git a/examples/chinese/main.go b/examples/chinese/main.go
index 21ffc1b..37760c6 100644
--- a/examples/chinese/main.go
+++ b/examples/chinese/main.go
@@ -28,54 +28,16 @@ func main() {
} else if err = charts.InstallFont("noto", buf); err != nil {
panic(err)
}
+ // in this example we just change the global default font
charts.SetDefaultFont("noto")
+ // It's also possible to specify the font on the chart configuration (for example on the title, or legend specifically)
values := [][]float64{
- {
- 120,
- 132,
- 101,
- 134,
- 90,
- 230,
- 210,
- },
- {
- 220,
- 182,
- 191,
- 234,
- 290,
- 330,
- 310,
- },
- {
- 150,
- 232,
- 201,
- 154,
- 190,
- 330,
- 410,
- },
- {
- 320,
- 332,
- 301,
- 334,
- 390,
- 330,
- 320,
- },
- {
- 820,
- 932,
- 901,
- 934,
- 1290,
- 1330,
- 1320,
- },
+ {120, 132, 101, 134, 90, 230, 210},
+ {220, 182, 191, 234, 290, 330, 310},
+ {150, 232, 201, 154, 190, 330, 410},
+ {320, 332, 301, 334, 390, 330, 320},
+ {820, 932, 901, 934, 1290, 1330, 1320},
}
p, err := charts.LineRender(
values,
diff --git a/examples/funnel_chart-2/main.go b/examples/funnel_chart-2/main.go
index 124da94..c36583e 100644
--- a/examples/funnel_chart-2/main.go
+++ b/examples/funnel_chart-2/main.go
@@ -24,23 +24,19 @@ func writeFile(buf []byte) error {
func main() {
values := []float64{100, 80, 60, 40, 20, 10, 2}
- opt := charts.FunnelChartOption{}
- opt.SeriesList = charts.NewFunnelSeriesList(values)
+ opt := charts.NewFunnelChartOptionWithData(values)
opt.Title.Text = "Funnel"
opt.Legend.Data = []string{
"Show", "Click", "Visit", "Inquiry", "Order", "Pay", "Cancel",
}
opt.Legend.Padding = charts.Box{Left: 100}
- p, err := charts.NewPainter(charts.PainterOptions{
+ p := charts.NewPainter(charts.PainterOptions{
OutputFormat: charts.ChartOutputPNG,
Width: 600,
Height: 400,
})
- if err != nil {
- panic(err)
- }
- if _, err = charts.NewFunnelChart(p, opt).Render(); err != nil {
+ if err := p.FunnelChart(opt); err != nil {
panic(err)
}
diff --git a/examples/horizontal_bar_chart-2/main.go b/examples/horizontal_bar_chart-2/main.go
index 20f50bb..02962a2 100644
--- a/examples/horizontal_bar_chart-2/main.go
+++ b/examples/horizontal_bar_chart-2/main.go
@@ -27,8 +27,7 @@ func main() {
{20, 40, 60, 80, 100, 120, 140},
}
- opt := charts.HorizontalBarChartOption{}
- opt.SeriesList = charts.NewSeriesListDataFromValues(values, charts.ChartTypeHorizontalBar)
+ opt := charts.NewHorizontalBarChartOptionWithData(values)
opt.Title.Text = "World Population"
opt.Padding = charts.Box{
Top: 20,
@@ -47,15 +46,12 @@ func main() {
},
}
- p, err := charts.NewPainter(charts.PainterOptions{
+ p := charts.NewPainter(charts.PainterOptions{
OutputFormat: charts.ChartOutputPNG,
Width: 600,
Height: 400,
})
- if err != nil {
- panic(err)
- }
- if _, err = charts.NewHorizontalBarChart(p, opt).Render(); err != nil {
+ if err := p.HorizontalBarChart(opt); err != nil {
panic(err)
}
diff --git a/examples/line_chart-2/main.go b/examples/line_chart-2/main.go
index 1a22b20..439331f 100644
--- a/examples/line_chart-2/main.go
+++ b/examples/line_chart-2/main.go
@@ -30,11 +30,9 @@ func main() {
{820, 932, 901, 934, 1290, 1330, 1320},
}
- opt := charts.LineChartOption{}
- opt.SeriesList = charts.NewSeriesListDataFromValues(values, charts.ChartTypeLine)
+ opt := charts.NewLineChartOptionWithData(values)
opt.Title.Text = "Line"
opt.Title.FontStyle.FontSize = 16
-
opt.XAxis.Data = []string{
"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun",
}
@@ -45,17 +43,14 @@ func main() {
Left: 100,
}
opt.SymbolShow = charts.True()
- opt.StrokeWidth = 1.2
+ opt.LineStrokeWidth = 1.2
- p, err := charts.NewPainter(charts.PainterOptions{
+ p := charts.NewPainter(charts.PainterOptions{
OutputFormat: charts.ChartOutputPNG,
Width: 600,
Height: 400,
})
- if err != nil {
- panic(err)
- }
- if _, err = charts.NewLineChart(p, opt).Render(); err != nil {
+ if err := p.LineChart(opt); err != nil {
panic(err)
}
diff --git a/examples/line_chart-3/main.go b/examples/line_chart-3/main.go
index 7acac2e..ae5a7cc 100644
--- a/examples/line_chart-3/main.go
+++ b/examples/line_chart-3/main.go
@@ -36,8 +36,7 @@ func main() {
p, err := charts.LineRender(
values,
charts.ThemeNameOptionFunc(charts.ThemeVividLight), // custom color theme
- charts.WidthOptionFunc(800),
- charts.HeightOptionFunc(600),
+ charts.DimensionsOptionFunc(800, 600),
charts.TitleOptionFunc(charts.TitleOption{
Text: "Line Chart Demo",
Offset: charts.OffsetCenter,
@@ -45,7 +44,7 @@ func main() {
charts.LegendOptionFunc(charts.LegendOption{
Data: []string{"Critical", "High", "Medium", "Low"},
// Legend Vertical, on the right, and with smaller font to give more space for data
- Vertical: true,
+ Vertical: charts.True(),
Offset: charts.OffsetRight,
Align: charts.AlignRight,
FontStyle: charts.FontStyle{
diff --git a/examples/line_chart-4/main.go b/examples/line_chart-4/main.go
index 780307b..b57eddf 100644
--- a/examples/line_chart-4/main.go
+++ b/examples/line_chart-4/main.go
@@ -46,7 +46,7 @@ func main() {
panic(err)
}
- opt := charts.LineChartOption{}
+ opt := charts.NewLineChartOptionWithData(values)
opt.Theme = charts.GetTheme(charts.ThemeAnt)
opt.Padding = charts.Box{
Top: 20,
@@ -54,7 +54,6 @@ func main() {
Right: 20,
Bottom: 10,
}
- opt.SeriesList = charts.NewSeriesListDataFromValues(values, charts.ChartTypeLine)
opt.Title.Text = "Canon RF Zoom Lenses"
opt.Title.Offset = charts.OffsetCenter
opt.Title.FontStyle.FontSize = 16
@@ -80,18 +79,15 @@ func main() {
}
opt.Legend.Show = charts.False()
opt.SymbolShow = charts.False()
- opt.StrokeWidth = 1.5
+ opt.LineStrokeWidth = 1.5
- p, err := charts.NewPainter(charts.PainterOptions{
+ p := charts.NewPainter(charts.PainterOptions{
OutputFormat: charts.ChartOutputPNG,
// positions drawn below depend on the canvas size set here
Width: 600,
Height: 400,
})
- if err != nil {
- panic(err)
- }
- if _, err = charts.NewLineChart(p, opt).Render(); err != nil {
+ if err = p.LineChart(opt); err != nil {
panic(err)
}
@@ -101,44 +97,35 @@ func main() {
FontColor: drawing.ColorBlack,
Font: charts.GetDefaultFont(),
}
- p.SetFontStyle(fontStyle)
- //p.TextRotation("f/stop", 10, 170, chartdraw.DegreesToRadians(90))
+ //p.Text("f/stop", 10, 170, chartdraw.DegreesToRadians(90), fontStyle)
fontStyle.FontColor = opt.Theme.GetSeriesColor(0)
- p.SetFontStyle(fontStyle)
- p.Text(labels[0], 420, 84)
+ p.Text(labels[0], 420, 84, 0, fontStyle)
fontStyle.FontColor = opt.Theme.GetSeriesColor(1)
- p.SetFontStyle(fontStyle)
- p.Text(labels[1], 45, 284)
+ p.Text(labels[1], 45, 284, 0, fontStyle)
fontStyle.FontColor = opt.Theme.GetSeriesColor(2)
- p.SetFontStyle(fontStyle)
- p.Text(labels[2], 140, 230)
+ p.Text(labels[2], 140, 230, 0, fontStyle)
fontStyle.FontColor = opt.Theme.GetSeriesColor(3)
- p.SetFontStyle(fontStyle)
- p.Text(labels[3], 160, 155)
+ p.Text(labels[3], 160, 155, 0, fontStyle)
fontStyle.FontSize = 8
fontStyle.FontColor = opt.Theme.GetSeriesColor(0)
- p.SetFontStyle(fontStyle)
- p.Text("f/4.5", 42, 220)
- p.Text("f/5.0", 105, 196)
- p.Text("f/6.3", 370, 137)
- p.Text("f/7.1", 570, 100)
+ p.Text("f/4.5", 42, 220, 0, fontStyle)
+ p.Text("f/5.0", 105, 196, 0, fontStyle)
+ p.Text("f/6.3", 370, 137, 0, fontStyle)
+ p.Text("f/7.1", 570, 100, 0, fontStyle)
fontStyle.FontColor = opt.Theme.GetSeriesColor(1)
- p.SetFontStyle(fontStyle)
- p.Text("f/2.8", 5, 298)
+ p.Text("f/2.8", 5, 298, 0, fontStyle)
fontStyle.FontColor = opt.Theme.GetSeriesColor(2)
- p.SetFontStyle(fontStyle)
- p.Text("f/4.0", 40, 244)
+ p.Text("f/4.0", 40, 244, 0, fontStyle)
fontStyle.FontColor = opt.Theme.GetSeriesColor(3)
- p.SetFontStyle(fontStyle)
- p.Text("f/5.6", 92, 168)
+ p.Text("f/5.6", 92, 168, 0, fontStyle)
if buf, err := p.Bytes(); err != nil {
panic(err)
diff --git a/examples/multiple_charts-1/main.go b/examples/multiple_charts-1/main.go
index f2254ee..58da72a 100644
--- a/examples/multiple_charts-1/main.go
+++ b/examples/multiple_charts-1/main.go
@@ -24,14 +24,11 @@ func writeFile(buf []byte) error {
}
func main() {
- p, err := charts.NewPainter(charts.PainterOptions{
+ p := charts.NewPainter(charts.PainterOptions{
OutputFormat: charts.ChartOutputPNG,
Width: 800,
Height: 600,
})
- if err != nil {
- panic(err)
- }
p.SetBackground(800, 600, drawing.ColorWhite)
// set the space and theme for each chart
topCenterPainter := p.Child(charts.PainterBoxOption(chartdraw.NewBox(0, 0, 800, 300)),
@@ -59,7 +56,7 @@ func main() {
"Email", "Union Ads", "Video Ads", "Direct", "Search Engine",
},
},
- SeriesList: charts.NewSeriesListDataFromValues([][]float64{
+ SeriesList: charts.NewSeriesListLine([][]float64{
{120, 132, 101, 134, 90, 230, 210},
{220, 182, 191, 234, 290, 330, 310},
{150, 232, 201, 154, 190, 330, 410},
@@ -69,13 +66,13 @@ func main() {
}
// render the same chart in each spot for the demo
- if _, err = charts.NewLineChart(bottomLeftPainter, lineOpt).Render(); err != nil {
+ if err := bottomLeftPainter.LineChart(lineOpt); err != nil {
panic(err)
}
- if _, err = charts.NewLineChart(bottomRightPainter, lineOpt).Render(); err != nil {
+ if err := bottomRightPainter.LineChart(lineOpt); err != nil {
panic(err)
}
- if _, err = charts.NewLineChart(topCenterPainter, lineOpt).Render(); err != nil {
+ if err := topCenterPainter.LineChart(lineOpt); err != nil {
panic(err)
}
diff --git a/examples/multiple_charts-2/main.go b/examples/multiple_charts-2/main.go
index 091eda3..c610ec5 100644
--- a/examples/multiple_charts-2/main.go
+++ b/examples/multiple_charts-2/main.go
@@ -78,13 +78,13 @@ func main() {
},
},
},
- SeriesList: charts.NewSeriesListDataFromValues([][]float64{
+ SeriesList: charts.NewSeriesListHorizontalBar([][]float64{
{70, 90, 110, 130},
{80, 100, 120, 140},
- }, charts.ChartTypeHorizontalBar),
+ }),
}
p = p.Child(charts.PainterBoxOption(chartdraw.NewBox(0, 200, 600, 200)))
- if _, err = charts.NewHorizontalBarChart(p, hBarOpt).Render(); err != nil {
+ if err = p.HorizontalBarChart(hBarOpt); err != nil {
panic(err)
}
diff --git a/examples/multiple_charts-3/main.go b/examples/multiple_charts-3/main.go
index 6ed8d22..87277d9 100644
--- a/examples/multiple_charts-3/main.go
+++ b/examples/multiple_charts-3/main.go
@@ -59,10 +59,10 @@ func main() {
opt.Children = []charts.ChartOption{
{
Box: chartdraw.NewBox(10, 200, 500, 200),
- SeriesList: charts.NewSeriesListDataFromValues([][]float64{
+ SeriesList: charts.NewSeriesListHorizontalBar([][]float64{
{70, 90, 110, 130},
{80, 100, 120, 140},
- }, charts.ChartTypeHorizontalBar),
+ }),
Legend: charts.LegendOption{
Data: []string{
"2011", "2012",
diff --git a/examples/pie_chart-1/main.go b/examples/pie_chart-1/main.go
index 2cea2e1..7a45735 100644
--- a/examples/pie_chart-1/main.go
+++ b/examples/pie_chart-1/main.go
@@ -48,7 +48,7 @@ func main() {
Data: []string{
"Search Engine", "Direct", "Email", "Union Ads", "Video Ads",
},
- Vertical: true,
+ Vertical: charts.True(),
Offset: charts.OffsetStr{
Left: "80%",
Top: charts.PositionBottom,
@@ -57,7 +57,6 @@ func main() {
FontSize: 10,
},
}),
- charts.PieSeriesShowLabel(),
)
if err != nil {
panic(err)
diff --git a/examples/pie_chart-2/main.go b/examples/pie_chart-2/main.go
index 9277297..419412b 100644
--- a/examples/pie_chart-2/main.go
+++ b/examples/pie_chart-2/main.go
@@ -26,8 +26,7 @@ func main() {
1048, 735, 580, 484, 300,
}
- opt := charts.PieChartOption{}
- opt.SeriesList = charts.NewPieSeriesList(values)
+ opt := charts.NewPieChartOptionWithData(values)
opt.Title = charts.TitleOption{
Text: "Rainfall vs Evaporation",
Subtext: "(Fake Data)",
@@ -49,7 +48,7 @@ func main() {
Data: []string{
"Search Engine", "Direct", "Email", "Union Ads", "Video Ads",
},
- Vertical: true,
+ Vertical: charts.True(),
Offset: charts.OffsetStr{
Left: "80%",
Top: charts.PositionBottom,
@@ -58,19 +57,13 @@ func main() {
FontSize: 10,
},
}
- for index := range opt.SeriesList {
- opt.SeriesList[index].Label.Show = true
- }
- p, err := charts.NewPainter(charts.PainterOptions{
+ p := charts.NewPainter(charts.PainterOptions{
OutputFormat: charts.ChartOutputPNG,
Width: 600,
Height: 400,
})
- if err != nil {
- panic(err)
- }
- if _, err = charts.NewPieChart(p, opt).Render(); err != nil {
+ if err := p.PieChart(opt); err != nil {
panic(err)
}
diff --git a/examples/radar_chart-2/main.go b/examples/radar_chart-2/main.go
index 95875cc..9b55d4f 100644
--- a/examples/radar_chart-2/main.go
+++ b/examples/radar_chart-2/main.go
@@ -27,8 +27,23 @@ func main() {
{5000, 14000, 28000, 26000, 42000, 21000},
}
- opt := charts.RadarChartOption{}
- opt.SeriesList = charts.NewSeriesListDataFromValues(values, charts.ChartTypeRadar)
+ opt := charts.NewRadarChartOptionWithData(values,
+ []string{
+ "Sales",
+ "Administration",
+ "Information Technology",
+ "Customer Support",
+ "Development",
+ "Marketing",
+ },
+ []float64{
+ 6500,
+ 16000,
+ 30000,
+ 38000,
+ 52000,
+ 25000,
+ })
opt.Title = charts.TitleOption{
Text: "Basic Radar Chart",
FontStyle: charts.FontStyle{
@@ -41,31 +56,13 @@ func main() {
},
Offset: charts.OffsetRight,
}
- opt.RadarIndicators = charts.NewRadarIndicators([]string{
- "Sales",
- "Administration",
- "Information Technology",
- "Customer Support",
- "Development",
- "Marketing",
- }, []float64{
- 6500,
- 16000,
- 30000,
- 38000,
- 52000,
- 25000,
- })
- p, err := charts.NewPainter(charts.PainterOptions{
+ p := charts.NewPainter(charts.PainterOptions{
OutputFormat: charts.ChartOutputPNG,
Width: 600,
Height: 400,
})
- if err != nil {
- panic(err)
- }
- if _, err = charts.NewRadarChart(p, opt).Render(); err != nil {
+ if err := p.RadarChart(opt); err != nil {
panic(err)
}
diff --git a/examples/table-1/main.go b/examples/table-1/main.go
index 4fcf44a..cafa685 100644
--- a/examples/table-1/main.go
+++ b/examples/table-1/main.go
@@ -25,7 +25,7 @@ func writeFile(buf []byte, filename string) error {
}
func main() {
- charts.SetDefaultWidth(810)
+ charts.SetDefaultChartDimensions(810, 0) // 0 for height will leave it unchanged
header := []string{
"Name",
"Age",
@@ -64,7 +64,7 @@ func main() {
3: 2,
4: 2,
}
- p, err := charts.TableRender(
+ p, err := charts.TableRenderValues(
header,
data,
spans,
@@ -80,7 +80,7 @@ func main() {
}
bgColor := charts.Color{R: 28, G: 28, B: 32, A: 255}
- p, err = charts.TableOptionRender(charts.TableChartOption{
+ p, err = charts.TableOptionRenderDirect(charts.TableChartOption{
Header: []string{"Name", "Price", "Change"},
BackgroundColor: bgColor,
HeaderBackgroundColor: charts.Color{R: 80, G: 80, B: 80, A: 255},
diff --git a/examples/web-1/main.go b/examples/web-1/main.go
index 971b76f..b91d9c6 100644
--- a/examples/web-1/main.go
+++ b/examples/web-1/main.go
@@ -73,8 +73,7 @@ func handler(w http.ResponseWriter, req *http.Request, chartOptions []charts.Cha
theme := query.Get("theme")
width, _ := strconv.Atoi(query.Get("width"))
height, _ := strconv.Atoi(query.Get("height"))
- charts.SetDefaultWidth(width)
- charts.SetDefaultWidth(height)
+ charts.SetDefaultChartDimensions(width, height)
bytesList := make([][]byte, 0)
for _, opt := range chartOptions {
opt.Theme = charts.GetTheme(theme)
@@ -97,14 +96,10 @@ func handler(w http.ResponseWriter, req *http.Request, chartOptions []charts.Cha
bytesList = append(bytesList, buf)
}
- p, err := charts.TableOptionRender(charts.TableChartOption{
+ p, err := charts.TableOptionRenderDirect(charts.TableChartOption{
OutputFormat: charts.ChartOutputSVG,
Header: []string{
- "Name",
- "Age",
- "Address",
- "Tag",
- "Action",
+ "Name", "Age", "Address", "Tag", "Action",
},
Data: [][]string{
{
@@ -155,72 +150,22 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
},
Legend: charts.LegendOption{
Data: []string{
- "Email",
- "Union Ads",
- "Video Ads",
- "Direct",
- "Search Engine",
+ "Email", "Union Ads", "Video Ads", "Direct", "Search Engine",
},
Padding: charts.Box{Left: 100},
},
XAxis: charts.XAxisOption{
Data: []string{
- "Mon",
- "Tue",
- "Wed",
- "Thu",
- "Fri",
- "Sat",
- "Sun",
+ "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,
- }),
- },
+ SeriesList: charts.NewSeriesListLine([][]float64{
+ {120, 132, 101, 134, 90, 230, 210},
+ {220, 182, 191, 234, 290, 330, 310},
+ {150, 232, 201, 154, 190, 330, 410},
+ {320, 332, 301, 334, 390, 330, 320},
+ {820, 932, 901, 934, 1290, 1330, 1320},
+ }),
},
// temperature line chart
{
@@ -235,46 +180,29 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
},
Legend: charts.LegendOption{
Data: []string{
- "Highest",
- "Lowest",
+ "Highest", "Lowest",
},
},
XAxis: charts.XAxisOption{
Data: []string{
- "Mon",
- "Tue",
- "Wed",
- "Thu",
- "Fri",
- "Sat",
- "Sun",
+ "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun",
},
BoundaryGap: charts.False(),
},
SeriesList: []charts.Series{
{
- Data: charts.NewSeriesDataFromValues([]float64{
- 14,
- 11,
- 13,
- 11,
- 12,
- 12,
- 7,
- }),
+ Data: []float64{
+ 14, 11, 13, 11, 12, 12, 7,
+ },
+ Type: charts.ChartTypeLine,
MarkPoint: charts.NewMarkPoint(charts.SeriesMarkDataTypeMax, charts.SeriesMarkDataTypeMin),
MarkLine: charts.NewMarkLine(charts.SeriesMarkDataTypeAverage),
},
{
- Data: charts.NewSeriesDataFromValues([]float64{
- 1,
- -2,
- 2,
- 5,
- 3,
- 2,
- 0,
- }),
+ Data: []float64{
+ 1, -2, 2, 5, 3, 2, 0,
+ },
+ Type: charts.ChartTypeLine,
MarkLine: charts.NewMarkLine(charts.SeriesMarkDataTypeAverage),
},
},
@@ -291,29 +219,15 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
},
XAxis: charts.XAxisOption{
Data: []string{
- "Mon",
- "Tue",
- "Wed",
- "Thu",
- "Fri",
- "Sat",
- "Sun",
+ "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun",
},
},
YAxis: []charts.YAxisOption{{
Min: charts.FloatPointer(0.0), // ensure y-axis starts at 0
}},
- SeriesList: []charts.Series{
- charts.NewSeriesFromValues([]float64{
- 120,
- 132,
- 101,
- 134,
- 90,
- 230,
- 210,
- }),
- },
+ SeriesList: charts.NewSeriesListLine([][]float64{
+ {120, 132, 101, 134, 90, 230, 210},
+ }),
FillArea: true,
},
// histogram
@@ -326,59 +240,29 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
},
XAxis: charts.XAxisOption{
Data: []string{
- "Mon",
- "Tue",
- "Wed",
- "Thu",
- "Fri",
- "Sat",
- "Sun",
+ "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun",
},
},
Legend: charts.LegendOption{
Data: []string{
- "Rainfall",
- "Evaporation",
+ "Rainfall", "Evaporation",
},
Icon: charts.IconRect,
},
SeriesList: []charts.Series{
- charts.NewSeriesFromValues([]float64{
- 120,
- 200,
- 150,
- 80,
- 70,
- 110,
- 130,
- }, charts.ChartTypeBar),
{
+ Data: []float64{
+ 120, 200, 150, 80, 70, 110, 130,
+ },
Type: charts.ChartTypeBar,
- Data: []charts.SeriesData{
- {
- Value: 100,
- },
- {
- Value: 190,
- },
- {
- Value: 230,
- },
- {
- Value: 140,
- },
- {
- Value: 100,
- },
- {
- Value: 200,
- },
- {
- Value: 180,
- },
+ },
+ {
+ Data: []float64{
+ 100, 190, 230, 140, 100, 200, 180,
},
+ Type: charts.ChartTypeBar,
Label: charts.SeriesLabel{
- Show: true,
+ Show: charts.True(),
Position: charts.PositionBottom,
},
},
@@ -400,46 +284,20 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
},
Legend: charts.LegendOption{
Data: []string{
- "2011",
- "2012",
+ "2011", "2012",
},
},
YAxis: []charts.YAxisOption{
{
Data: []string{
- "Brazil",
- "Indonesia",
- "USA",
- "India",
- "China",
- "World",
+ "Brazil", "Indonesia", "USA", "India", "China", "World",
},
},
},
- SeriesList: []charts.Series{
- {
- Type: charts.ChartTypeHorizontalBar,
- Data: charts.NewSeriesDataFromValues([]float64{
- 18203,
- 23489,
- 29034,
- 104970,
- 131744,
- 630230,
- }),
- },
- {
- Type: charts.ChartTypeHorizontalBar,
- Data: charts.NewSeriesDataFromValues([]float64{
- 19325,
- 23438,
- 31000,
- 121594,
- 134141,
- 681807,
- }),
- },
- },
+ SeriesList: charts.NewSeriesListHorizontalBar([][]float64{
+ {18203, 23489, 29034, 104970, 131744, 630230},
+ {19325, 23438, 31000, 121594, 134141, 681807},
+ }),
},
// histogram+marker
{
@@ -461,43 +319,20 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
},
XAxis: charts.XAxisOption{
Data: []string{
- "Jan",
- "Feb",
- "Mar",
- "Apr",
- "May",
- "Jun",
- "Jul",
- "Aug",
- "Sep",
- "Oct",
- "Nov",
- "Dec",
+ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
},
},
Legend: charts.LegendOption{
Data: []string{
- "Rainfall",
- "Evaporation",
+ "Rainfall", "Evaporation",
},
},
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,
- }),
+ Data: []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,
@@ -508,20 +343,9 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
},
{
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,
- }),
+ Data: []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,
@@ -542,25 +366,12 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
},
XAxis: charts.XAxisOption{
Data: []string{
- "Jan",
- "Feb",
- "Mar",
- "Apr",
- "May",
- "Jun",
- "Jul",
- "Aug",
- "Sep",
- "Oct",
- "Nov",
- "Dec",
+ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
},
},
Legend: charts.LegendOption{
Data: []string{
- "Evaporation",
- "Precipitation",
- "Temperature",
+ "Evaporation", "Precipitation", "Temperature",
},
Padding: charts.Box{Left: 100},
},
@@ -574,59 +385,16 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
AxisColor: charts.Color{R: 250, G: 200, B: 88, 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,
- }),
- },
- {
- 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,
- }),
- },
- {
- 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,
- }),
- YAxisIndex: 1,
+ SeriesList: append(charts.NewSeriesListBar([][]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},
+ {2.6, 5.9, 9.0, 26.4, 28.7, 70.7, 175.6, 182.2, 48.7, 18.8, 6.0, 2.3},
+ }), charts.Series{
+ Type: charts.ChartTypeLine,
+ Data: []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,
},
- },
+ YAxisIndex: 1,
+ }),
},
// pie chart
{
@@ -642,26 +410,15 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
},
},
Legend: charts.LegendOption{
- Vertical: true,
+ Vertical: charts.True(),
Data: []string{
- "Search Engine",
- "Direct",
- "Email",
- "Union Ads",
- "Video Ads",
+ "Search Engine", "Direct", "Email", "Union Ads", "Video Ads",
},
Offset: charts.OffsetLeft,
},
- SeriesList: charts.NewPieSeriesList([]float64{
- 1048,
- 735,
- 580,
- 484,
- 300,
+ SeriesList: charts.NewSeriesListPie([]float64{
+ 1048, 735, 580, 484, 300,
}, charts.PieSeriesOption{
- Label: charts.SeriesLabel{
- Show: true,
- },
Radius: "35%",
}),
},
@@ -675,8 +432,7 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
},
Legend: charts.LegendOption{
Data: []string{
- "Allocated Budget",
- "Actual Spending",
+ "Allocated Budget", "Actual Spending",
},
Padding: charts.Box{Left: 100},
},
@@ -706,30 +462,10 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
Max: 25000,
},
},
- SeriesList: charts.SeriesList{
- {
- Type: charts.ChartTypeRadar,
- Data: charts.NewSeriesDataFromValues([]float64{
- 4200,
- 3000,
- 20000,
- 35000,
- 50000,
- 18000,
- }),
- },
- {
- Type: charts.ChartTypeRadar,
- Data: charts.NewSeriesDataFromValues([]float64{
- 5000,
- 14000,
- 28000,
- 26000,
- 42000,
- 21000,
- }),
- },
- },
+ SeriesList: charts.NewSeriesListRadar([][]float64{
+ {4200, 3000, 20000, 35000, 50000, 18000},
+ {5000, 14000, 28000, 26000, 42000, 21000},
+ }),
},
// funnel chart
{
@@ -738,48 +474,34 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
},
Legend: charts.LegendOption{
Data: []string{
- "Show",
- "Click",
- "Visit",
- "Inquiry",
- "Order",
+ "Show", "Click", "Visit", "Inquiry", "Order",
},
},
SeriesList: []charts.Series{
{
Type: charts.ChartTypeFunnel,
Name: "Show",
- Data: charts.NewSeriesDataFromValues([]float64{
- 100,
- }),
+ Data: []float64{100},
},
{
Type: charts.ChartTypeFunnel,
Name: "Click",
- Data: charts.NewSeriesDataFromValues([]float64{
- 80,
- }),
+ Data: []float64{80},
},
{
Type: charts.ChartTypeFunnel,
Name: "Visit",
- Data: charts.NewSeriesDataFromValues([]float64{
- 60,
- }),
+ Data: []float64{60},
},
{
Type: charts.ChartTypeFunnel,
Name: "Inquiry",
- Data: charts.NewSeriesDataFromValues([]float64{
- 40,
- }),
+ Data: []float64{40},
},
{
Type: charts.ChartTypeFunnel,
Name: "Order",
- Data: charts.NewSeriesDataFromValues([]float64{
- 20,
- }),
+ Data: []float64{20},
},
},
},
@@ -790,10 +512,7 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
Top: "-90",
},
Data: []string{
- "Milk Tea",
- "Matcha Latte",
- "Cheese Cocoa",
- "Walnut Brownie",
+ "Milk Tea", "Matcha Latte", "Cheese Cocoa", "Walnut Brownie",
},
},
Padding: charts.Box{
@@ -804,12 +523,7 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
},
XAxis: charts.XAxisOption{
Data: []string{
- "2012",
- "2013",
- "2014",
- "2015",
- "2016",
- "2017",
+ "2012", "2013", "2014", "2015", "2016", "2017",
},
},
YAxis: []charts.YAxisOption{
@@ -819,49 +533,21 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
Max: charts.FloatPointer(90),
},
},
- SeriesList: []charts.Series{
- charts.NewSeriesFromValues([]float64{
- 56.5,
- 82.1,
- 88.7,
- 70.1,
- 53.4,
- 85.1,
+ SeriesList: append(
+ charts.NewSeriesListLine([][]float64{
+ {56.5, 82.1, 88.7, 70.1, 53.4, 85.1},
+ {51.1, 51.4, 55.1, 53.3, 73.8, 68.7},
}),
- 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),
- },
+ charts.NewSeriesListBar([][]float64{
+ {40.1, 62.2, 69.5, 36.4, 45.2, 32.5},
+ {25.2, 37.1, 41.2, 18, 33.9, 49.1},
+ })...),
Children: []charts.ChartOption{
{
Legend: charts.LegendOption{
Show: charts.False(),
Data: []string{
- "Milk Tea",
- "Matcha Latte",
- "Cheese Cocoa",
- "Walnut Brownie",
+ "Milk Tea", "Matcha Latte", "Cheese Cocoa", "Walnut Brownie",
},
},
Box: charts.Box{
@@ -870,15 +556,9 @@ func indexHandler(w http.ResponseWriter, req *http.Request) {
Right: 500,
Bottom: 120,
},
- SeriesList: charts.NewPieSeriesList([]float64{
- 435.9,
- 354.3,
- 285.9,
- 204.5,
+ SeriesList: charts.NewSeriesListPie([]float64{
+ 435.9, 354.3, 285.9, 204.5,
}, charts.PieSeriesOption{
- Label: charts.SeriesLabel{
- Show: true,
- },
Radius: "35%",
}),
},
diff --git a/font_test.go b/font_test.go
index fb9ed60..75892e3 100644
--- a/font_test.go
+++ b/font_test.go
@@ -31,12 +31,11 @@ func TestGetPreferredFont(t *testing.T) {
func TestCustomFontSizeRender(t *testing.T) {
t.Parallel()
- p, err := NewPainter(PainterOptions{
+ p := NewPainter(PainterOptions{
OutputFormat: ChartOutputSVG,
Width: 600,
Height: 400,
}, PainterThemeOption(GetTheme(ThemeLight)))
- require.NoError(t, err)
opt := makeBasicLineChartOption()
opt.XAxis.FontStyle.FontSize = 4.0
@@ -50,9 +49,9 @@ func TestCustomFontSizeRender(t *testing.T) {
opt.Title.FontStyle.FontSize = 4.0
opt.Legend.FontStyle.FontSize = 4.0
- _, err = NewLineChart(p, opt).Render()
+ err := p.LineChart(opt)
require.NoError(t, err)
data, err := p.Bytes()
require.NoError(t, err)
- assertEqualSVG(t, "", data)
+ assertEqualSVG(t, "", data)
}
diff --git a/funnel_chart.go b/funnel_chart.go
index a4505db..6636219 100644
--- a/funnel_chart.go
+++ b/funnel_chart.go
@@ -4,8 +4,6 @@ import (
"errors"
"github.com/golang/freetype/truetype"
-
- "github.com/go-analyze/charts/chartdraw"
)
type funnelChart struct {
@@ -13,25 +11,24 @@ type funnelChart struct {
opt *FunnelChartOption
}
-// NewFunnelSeriesList returns a series list for funnel
-func NewFunnelSeriesList(values []float64) SeriesList {
- seriesList := make(SeriesList, len(values))
- for index, value := range values {
- seriesList[index] = NewSeriesFromValues([]float64{
- value,
- }, ChartTypeFunnel)
- }
- return seriesList
-}
-
-// NewFunnelChart returns a funnel chart renderer
-func NewFunnelChart(p *Painter, opt FunnelChartOption) *funnelChart {
+// newFunnelChart returns a funnel chart renderer
+func newFunnelChart(p *Painter, opt FunnelChartOption) *funnelChart {
return &funnelChart{
p: p,
opt: &opt,
}
}
+// NewFunnelChartOptionWithData returns an initialized FunnelChartOption with the SeriesList set for the provided data slice.
+func NewFunnelChartOptionWithData(data []float64) FunnelChartOption {
+ return FunnelChartOption{
+ SeriesList: NewSeriesListFunnel(data),
+ Padding: defaultPadding,
+ Theme: GetDefaultTheme(),
+ Font: GetDefaultFont(),
+ }
+}
+
type FunnelChartOption struct {
// Theme specifies the colors used for the chart.
Theme ColorPalette
@@ -50,16 +47,8 @@ type FunnelChartOption struct {
func (f *funnelChart) render(result *defaultRenderResult, seriesList SeriesList) (Box, error) {
opt := f.opt
seriesPainter := result.seriesPainter
- max := seriesList[0].Data[0].Value
+ max := seriesList[0].Data[0]
min := float64(0)
- for _, item := range seriesList {
- if item.Max != nil {
- max = *item.Max
- }
- if item.Min != nil {
- min = *item.Min
- }
- }
theme := opt.Theme
gap := 2
height := seriesPainter.Height()
@@ -77,7 +66,7 @@ func (f *funnelChart) render(result *defaultRenderResult, seriesList SeriesList)
seriesNames := seriesList.Names()
offset := max - min
for index, item := range seriesList {
- value := item.Data[0].Value
+ value := item.Data[0]
// if the maximum and minimum are consistent it's 100%
widthPercent := 100.0
if offset != 0 {
@@ -90,7 +79,7 @@ func (f *funnelChart) render(result *defaultRenderResult, seriesList SeriesList)
if max != 0 {
percent = value / max
}
- textList[index] = NewFunnelLabelFormatter(seriesNames, item.Label.Formatter)(index, value, percent)
+ textList[index] = labelFormatFunnel(seriesNames, item.Label.Formatter, index, value, percent)
}
for index, w := range widthList {
@@ -125,23 +114,19 @@ func (f *funnelChart) render(result *defaultRenderResult, seriesList SeriesList)
Y: y,
},
}
- color := theme.GetSeriesColor(series.index)
- seriesPainter.OverrideDrawingStyle(chartdraw.Style{
- FillColor: color,
- }).FillArea(points)
+ seriesPainter.FillArea(points, theme.GetSeriesColor(series.index))
- // text
text := textList[index]
- seriesPainter.OverrideFontStyle(FontStyle{
+ fontStyle := FontStyle{
FontColor: theme.GetTextColor(),
FontSize: labelFontSize,
Font: opt.Font,
- })
- textBox := seriesPainter.MeasureText(text)
+ }
+ textBox := seriesPainter.MeasureText(text, 0, fontStyle)
textX := width>>1 - textBox.Width()>>1
textY := y + h>>1
- seriesPainter.Text(text, textX, textY)
+ seriesPainter.Text(text, textX, textY, 0, fontStyle)
y += h + gap
}
@@ -156,19 +141,19 @@ func (f *funnelChart) Render() (Box, error) {
}
renderResult, err := defaultRender(p, defaultRenderOption{
- Theme: opt.Theme,
- Padding: opt.Padding,
- SeriesList: opt.SeriesList,
- XAxis: XAxisOption{
+ theme: opt.Theme,
+ padding: opt.Padding,
+ seriesList: opt.SeriesList,
+ xAxis: &XAxisOption{
Show: False(),
},
- YAxis: []YAxisOption{
+ yAxis: []YAxisOption{
{
Show: False(),
},
},
- Title: opt.Title,
- Legend: opt.Legend,
+ title: opt.Title,
+ legend: &f.opt.Legend,
})
if err != nil {
return BoxZero, err
diff --git a/funnel_chart_test.go b/funnel_chart_test.go
index be86313..9808cf5 100644
--- a/funnel_chart_test.go
+++ b/funnel_chart_test.go
@@ -4,12 +4,13 @@ import (
"strconv"
"testing"
+ "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func makeBasicFunnelChartOption() FunnelChartOption {
return FunnelChartOption{
- SeriesList: NewFunnelSeriesList([]float64{
+ SeriesList: NewSeriesListFunnel([]float64{
100, 80, 60, 40, 20,
}),
Legend: LegendOption{
@@ -23,6 +24,19 @@ func makeBasicFunnelChartOption() FunnelChartOption {
}
}
+func TestNewFunnelChartOptionWithData(t *testing.T) {
+ t.Parallel()
+
+ opt := NewFunnelChartOptionWithData([]float64{12, 24, 48})
+
+ assert.Len(t, opt.SeriesList, 3)
+ assert.Equal(t, ChartTypeFunnel, opt.SeriesList[0].Type)
+ assert.Equal(t, defaultPadding, opt.Padding)
+
+ p := NewPainter(PainterOptions{})
+ assert.NoError(t, p.FunnelChart(opt))
+}
+
func TestFunnelChart(t *testing.T) {
t.Parallel()
@@ -36,13 +50,13 @@ func TestFunnelChart(t *testing.T) {
name: "default",
defaultTheme: true,
makeOptions: makeBasicFunnelChartOption,
- result: "",
+ result: "",
},
{
name: "themed",
defaultTheme: false,
makeOptions: makeBasicFunnelChartOption,
- result: "",
+ result: "",
},
}
@@ -54,21 +68,18 @@ func TestFunnelChart(t *testing.T) {
}
if tt.defaultTheme {
t.Run(strconv.Itoa(i)+"-"+tt.name, func(t *testing.T) {
- p, err := NewPainter(painterOptions)
- require.NoError(t, err)
+ p := NewPainter(painterOptions)
validateFunnelChartRender(t, p, tt.makeOptions(), tt.result)
})
} else {
t.Run(strconv.Itoa(i)+"-"+tt.name+"-painter", func(t *testing.T) {
- p, err := NewPainter(painterOptions, PainterThemeOption(GetTheme(ThemeVividDark)))
- require.NoError(t, err)
+ p := NewPainter(painterOptions, PainterThemeOption(GetTheme(ThemeVividDark)))
validateFunnelChartRender(t, p, tt.makeOptions(), tt.result)
})
t.Run(strconv.Itoa(i)+"-"+tt.name+"-options", func(t *testing.T) {
- p, err := NewPainter(painterOptions)
- require.NoError(t, err)
+ p := NewPainter(painterOptions)
opt := tt.makeOptions()
opt.Theme = GetTheme(ThemeVividDark)
@@ -81,7 +92,7 @@ func TestFunnelChart(t *testing.T) {
func validateFunnelChartRender(t *testing.T, p *Painter, opt FunnelChartOption, expectedResult string) {
t.Helper()
- _, err := NewFunnelChart(p, opt).Render()
+ err := p.FunnelChart(opt)
require.NoError(t, err)
data, err := p.Bytes()
require.NoError(t, err)
diff --git a/grid.go b/grid.go
deleted file mode 100644
index 2a752af..0000000
--- a/grid.go
+++ /dev/null
@@ -1,74 +0,0 @@
-package charts
-
-import (
- "github.com/go-analyze/charts/chartdraw"
-)
-
-type gridPainter struct {
- p *Painter
- opt *GridPainterOption
-}
-
-type GridPainterOption struct {
- // StrokeWidth is the grid line width.
- StrokeWidth float64
- // StrokeColor is the grid line color.
- StrokeColor Color
- // ColumnSpans specifies the span for each column.
- ColumnSpans []int
- // Columns is the count of columns in the grid.
- Columns int
- // Rows are the count of rows in the grid.
- Rows int
- // IgnoreFirstRow can be set to true to ignore the first row.
- IgnoreFirstRow bool
- // IgnoreLastRow can be set to true to ignore the last row.
- IgnoreLastRow bool
- // IgnoreFirstColumn can be set to true to ignore the first colum.
- IgnoreFirstColumn bool
- // IgnoreLastColumn can be set to true to ignore the last columns.
- IgnoreLastColumn bool
-}
-
-// NewGridPainter returns new a grid renderer
-func NewGridPainter(p *Painter, opt GridPainterOption) *gridPainter {
- return &gridPainter{
- p: p,
- opt: &opt,
- }
-}
-
-func (g *gridPainter) Render() (Box, error) {
- opt := g.opt
- ignoreColumnLines := make([]int, 0)
- if opt.IgnoreFirstColumn {
- ignoreColumnLines = append(ignoreColumnLines, 0)
- }
- if opt.IgnoreLastColumn {
- ignoreColumnLines = append(ignoreColumnLines, opt.Columns)
- }
- ignoreRowLines := make([]int, 0)
- if opt.IgnoreFirstRow {
- ignoreRowLines = append(ignoreRowLines, 0)
- }
- if opt.IgnoreLastRow {
- ignoreRowLines = append(ignoreRowLines, opt.Rows)
- }
- strokeWidth := opt.StrokeWidth
- if strokeWidth <= 0 {
- strokeWidth = 1
- }
-
- g.p.SetDrawingStyle(chartdraw.Style{
- StrokeWidth: strokeWidth,
- StrokeColor: opt.StrokeColor,
- })
- g.p.Grid(GridOption{
- Columns: opt.Columns,
- ColumnSpans: opt.ColumnSpans,
- Rows: opt.Rows,
- IgnoreColumnLines: ignoreColumnLines,
- IgnoreRowLines: ignoreRowLines,
- })
- return g.p.box, nil
-}
diff --git a/grid_test.go b/grid_test.go
deleted file mode 100644
index ffcf45a..0000000
--- a/grid_test.go
+++ /dev/null
@@ -1,65 +0,0 @@
-package charts
-
-import (
- "strconv"
- "testing"
-
- "github.com/stretchr/testify/require"
-
- "github.com/go-analyze/charts/chartdraw/drawing"
-)
-
-func TestGrid(t *testing.T) {
- t.Parallel()
-
- tests := []struct {
- render func(*Painter) ([]byte, error)
- result string
- }{
- {
- render: func(p *Painter) ([]byte, error) {
- _, err := NewGridPainter(p, GridPainterOption{
- StrokeColor: drawing.ColorBlack,
- Columns: 6,
- Rows: 6,
- IgnoreFirstRow: true,
- IgnoreLastRow: true,
- IgnoreFirstColumn: true,
- IgnoreLastColumn: true,
- }).Render()
- if err != nil {
- return nil, err
- }
- return p.Bytes()
- },
- result: "",
- },
- {
- render: func(p *Painter) ([]byte, error) {
- _, err := NewGridPainter(p, GridPainterOption{
- StrokeColor: drawing.ColorBlack,
- ColumnSpans: []int{2, 5, 3},
- Rows: 6,
- }).Render()
- if err != nil {
- return nil, err
- }
- return p.Bytes()
- },
- result: "",
- },
- }
- for i, tt := range tests {
- t.Run(strconv.Itoa(i), func(t *testing.T) {
- p, err := NewPainter(PainterOptions{
- OutputFormat: ChartOutputSVG,
- Width: 600,
- Height: 400,
- }, PainterThemeOption(GetTheme(ThemeLight)))
- require.NoError(t, err)
- data, err := tt.render(p)
- require.NoError(t, err)
- assertEqualSVG(t, tt.result, data)
- })
- }
-}
diff --git a/horizontal_bar_chart.go b/horizontal_bar_chart.go
index fc3e186..a868812 100644
--- a/horizontal_bar_chart.go
+++ b/horizontal_bar_chart.go
@@ -13,6 +13,19 @@ type horizontalBarChart struct {
opt *HorizontalBarChartOption
}
+// NewHorizontalBarChartOptionWithData returns an initialized HorizontalBarChartOption with the SeriesList set for the provided data slice.
+func NewHorizontalBarChartOptionWithData(data [][]float64) HorizontalBarChartOption {
+ sl := NewSeriesListHorizontalBar(data)
+ return HorizontalBarChartOption{
+ SeriesList: sl,
+ Padding: defaultPadding,
+ Theme: GetDefaultTheme(),
+ Font: GetDefaultFont(),
+ YAxis: make([]YAxisOption, sl.getYAxisCount()),
+ ValueFormatter: defaultValueFormatter,
+ }
+}
+
type HorizontalBarChartOption struct {
// Theme specifies the colors used for the chart.
Theme ColorPalette
@@ -32,10 +45,12 @@ type HorizontalBarChartOption struct {
Legend LegendOption
// BarHeight specifies the height of each horizontal bar.
BarHeight int
+ // ValueFormatter defines how float values should be rendered to strings, notably for numeric axis labels.
+ ValueFormatter ValueFormatter
}
-// NewHorizontalBarChart returns a horizontal bar chart renderer
-func NewHorizontalBarChart(p *Painter, opt HorizontalBarChartOption) *horizontalBarChart {
+// newHorizontalBarChart returns a horizontal bar chart renderer
+func newHorizontalBarChart(p *Painter, opt HorizontalBarChartOption) *horizontalBarChart {
return &horizontalBarChart{
p: p,
opt: &opt,
@@ -73,24 +88,19 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri
theme := opt.Theme
min, max := seriesList.GetMinMax(0)
- xRange := NewRange(p, seriesPainter.Width(), len(seriesList[0].Data), min, max, 1.0, 1.0)
+ xRange := newRange(p, getPreferredValueFormatter(opt.XAxis.ValueFormatter, opt.ValueFormatter),
+ seriesPainter.Width(), len(seriesList[0].Data), min, max, 1.0, 1.0)
seriesNames := seriesList.Names()
- var rendererList []Renderer
+ var rendererList []renderer
for index := range seriesList {
series := seriesList[index]
seriesColor := theme.GetSeriesColor(series.index)
divideValues := yRange.AutoDivide()
- var labelPainter *SeriesLabelPainter
- if series.Label.Show {
- labelPainter = NewSeriesLabelPainter(SeriesLabelPainterParams{
- P: seriesPainter,
- SeriesNames: seriesNames,
- Label: series.Label,
- Theme: opt.Theme,
- Font: opt.Font,
- })
+ var labelPainter *seriesLabelPainter
+ if flagIs(true, series.Label.Show) {
+ labelPainter = newSeriesLabelPainter(seriesPainter, seriesNames, series.Label, opt.Theme, opt.Font)
rendererList = append(rendererList, labelPainter)
}
for j, item := range series.Data {
@@ -105,18 +115,16 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri
y += index * (barHeight + barMargin)
}
- w := xRange.getHeight(item.Value)
+ w := xRange.getHeight(item)
fillColor := seriesColor
right := w
- seriesPainter.OverrideDrawingStyle(chartdraw.Style{
- FillColor: fillColor,
- }).Rect(chartdraw.Box{
+ seriesPainter.filledRect(chartdraw.Box{
Top: y,
Left: 0,
Right: right,
Bottom: y + barHeight,
IsSet: true,
- })
+ }, fillColor, fillColor, 0.0)
// if the label does not need to be displayed, return
if labelPainter == nil {
continue
@@ -129,17 +137,17 @@ func (h *horizontalBarChart) render(result *defaultRenderResult, seriesList Seri
fontStyle.FontColor = defaultDarkFontColor
}
}
- labelValue := LabelValue{
- Vertical: false,
- Index: index,
- Value: item.Value,
- X: right,
- Y: y + barHeight>>1,
- Offset: series.Label.Offset,
- FontStyle: fontStyle,
+ labelValue := labelValue{
+ vertical: false, // label beside bar
+ index: index,
+ value: item,
+ x: right,
+ y: y + (barHeight >> 1),
+ offset: series.Label.Offset,
+ fontStyle: fontStyle,
}
if series.Label.Position == PositionLeft {
- labelValue.X = 0
+ labelValue.x = 0
}
labelPainter.Add(labelValue)
}
@@ -158,14 +166,15 @@ func (h *horizontalBarChart) Render() (Box, error) {
}
renderResult, err := defaultRender(p, defaultRenderOption{
- Theme: opt.Theme,
- Padding: opt.Padding,
- SeriesList: opt.SeriesList,
- XAxis: opt.XAxis,
- YAxis: opt.YAxis,
- Title: opt.Title,
- Legend: opt.Legend,
- axisReversed: true,
+ theme: opt.Theme,
+ padding: opt.Padding,
+ seriesList: opt.SeriesList,
+ xAxis: &h.opt.XAxis,
+ yAxis: opt.YAxis,
+ title: opt.Title,
+ legend: &h.opt.Legend,
+ valueFormatter: opt.ValueFormatter,
+ axisReversed: true,
})
if err != nil {
return BoxZero, err
diff --git a/horizontal_bar_chart_test.go b/horizontal_bar_chart_test.go
index d155458..4297347 100644
--- a/horizontal_bar_chart_test.go
+++ b/horizontal_bar_chart_test.go
@@ -4,6 +4,7 @@ import (
"strconv"
"testing"
+ "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/go-analyze/charts/chartdraw/drawing"
@@ -17,10 +18,10 @@ func makeBasicHorizontalBarChartOption() HorizontalBarChartOption {
Bottom: 10,
Left: 10,
},
- SeriesList: NewSeriesListDataFromValues([][]float64{
+ SeriesList: NewSeriesListHorizontalBar([][]float64{
{18203, 23489, 29034, 104970, 131744, 630230},
{19325, 23438, 31000, 121594, 134141, 681807},
- }, ChartTypeHorizontalBar),
+ }),
Title: TitleOption{
Text: "World Population",
},
@@ -39,6 +40,23 @@ func makeBasicHorizontalBarChartOption() HorizontalBarChartOption {
}
}
+func TestNewHorizontalBarChartOptionWithData(t *testing.T) {
+ t.Parallel()
+
+ opt := NewHorizontalBarChartOptionWithData([][]float64{
+ {12, 24},
+ {24, 48},
+ })
+
+ assert.Len(t, opt.SeriesList, 2)
+ assert.Equal(t, ChartTypeHorizontalBar, opt.SeriesList[0].Type)
+ assert.Len(t, opt.YAxis, 1)
+ assert.Equal(t, defaultPadding, opt.Padding)
+
+ p := NewPainter(PainterOptions{})
+ assert.NoError(t, p.HorizontalBarChart(opt))
+}
+
func TestHorizontalBarChart(t *testing.T) {
t.Parallel()
@@ -52,13 +70,13 @@ func TestHorizontalBarChart(t *testing.T) {
name: "default",
defaultTheme: true,
makeOptions: makeBasicHorizontalBarChartOption,
- result: "",
+ result: "",
},
{
name: "themed",
defaultTheme: false,
makeOptions: makeBasicHorizontalBarChartOption,
- result: "",
+ result: "",
},
{
name: "custom_fonts",
@@ -74,7 +92,7 @@ func TestHorizontalBarChart(t *testing.T) {
opt.Title.FontStyle = customFont
return opt
},
- result: "",
+ result: "",
},
{
name: "value_labels",
@@ -83,11 +101,27 @@ func TestHorizontalBarChart(t *testing.T) {
opt := makeBasicHorizontalBarChartOption()
series := opt.SeriesList
for i := range series {
- series[i].Label.Show = true
+ series[i].Label.Show = True()
+ }
+ return opt
+ },
+ result: "",
+ },
+ {
+ name: "value_formatter",
+ defaultTheme: true,
+ makeOptions: func() HorizontalBarChartOption {
+ opt := makeBasicHorizontalBarChartOption()
+ series := opt.SeriesList
+ for i := range series {
+ series[i].Label.Show = True()
+ }
+ opt.ValueFormatter = func(f float64) string {
+ return "f"
}
return opt
},
- result: "",
+ result: "",
},
}
@@ -99,21 +133,18 @@ func TestHorizontalBarChart(t *testing.T) {
}
if tt.defaultTheme {
t.Run(strconv.Itoa(i)+"-"+tt.name, func(t *testing.T) {
- p, err := NewPainter(painterOptions)
- require.NoError(t, err)
+ p := NewPainter(painterOptions)
validateHorizontalBarChartRender(t, p, tt.makeOptions(), tt.result)
})
} else {
t.Run(strconv.Itoa(i)+"-"+tt.name+"-painter", func(t *testing.T) {
- p, err := NewPainter(painterOptions, PainterThemeOption(GetTheme(ThemeVividDark)))
- require.NoError(t, err)
+ p := NewPainter(painterOptions, PainterThemeOption(GetTheme(ThemeVividDark)))
validateHorizontalBarChartRender(t, p, tt.makeOptions(), tt.result)
})
t.Run(strconv.Itoa(i)+"-"+tt.name+"-options", func(t *testing.T) {
- p, err := NewPainter(painterOptions)
- require.NoError(t, err)
+ p := NewPainter(painterOptions)
opt := tt.makeOptions()
opt.Theme = GetTheme(ThemeVividDark)
@@ -126,7 +157,7 @@ func TestHorizontalBarChart(t *testing.T) {
func validateHorizontalBarChartRender(t *testing.T, p *Painter, opt HorizontalBarChartOption, expectedResult string) {
t.Helper()
- _, err := NewHorizontalBarChart(p, opt).Render()
+ err := p.HorizontalBarChart(opt)
require.NoError(t, err)
data, err := p.Bytes()
require.NoError(t, err)
diff --git a/legend.go b/legend.go
index 7f1cc6a..50eb27c 100644
--- a/legend.go
+++ b/legend.go
@@ -2,8 +2,6 @@ package charts
import (
"fmt"
-
- "github.com/go-analyze/charts/chartdraw"
)
type legendPainter struct {
@@ -29,8 +27,8 @@ type LegendOption struct {
Offset OffsetStr
// Align is the legend marker and text alignment, it can be 'left', 'right' or 'center', default is 'left'.
Align string
- // Vertical can be set to true to set the orientation to be vertical.
- Vertical bool
+ // Vertical can be set to *true to set the legend orientation to be vertical.
+ Vertical *bool
// Icon to show next to the labels. Can be 'rect' or 'dot'.
Icon string
// OverlayChart can be set to *true to render the legend over the chart. Ignored if Vertical is set to true (always overlapped).
@@ -47,8 +45,8 @@ func (opt *LegendOption) IsEmpty() bool {
return true
}
-// NewLegendPainter returns a legend renderer
-func NewLegendPainter(p *Painter, opt LegendOption) *legendPainter {
+// newLegendPainter returns a legend renderer
+func newLegendPainter(p *Painter, opt LegendOption) *legendPainter {
return &legendPainter{
p: p,
opt: &opt,
@@ -72,9 +70,10 @@ func (l *legendPainter) Render() (Box, error) {
if fontStyle.FontColor.IsZero() {
fontStyle.FontColor = theme.GetTextColor()
}
+ vertical := flagIs(true, opt.Vertical)
offset := opt.Offset
if offset.Left == "" {
- if opt.Vertical {
+ if vertical {
// in the vertical orientation it's more visually appealing to default to the right side or left side
if opt.Align != "" {
offset.Left = opt.Align
@@ -90,7 +89,6 @@ func (l *legendPainter) Render() (Box, error) {
padding.Top = 5
}
p := l.p.Child(PainterPaddingOption(padding))
- p.SetFontStyle(fontStyle)
// calculate the width and height of the display
measureList := make([]Box, len(opt.Data))
@@ -103,14 +101,14 @@ func (l *legendPainter) Render() (Box, error) {
maxTextWidth := 0
itemMaxHeight := 0
for index, text := range opt.Data {
- b := p.MeasureText(text)
+ b := p.MeasureText(text, 0, fontStyle)
if b.Width() > maxTextWidth {
maxTextWidth = b.Width()
}
if b.Height() > itemMaxHeight {
itemMaxHeight = b.Height()
}
- if opt.Vertical {
+ if flagIs(true, opt.Vertical) {
height += b.Height()
} else {
width += b.Width()
@@ -119,7 +117,7 @@ func (l *legendPainter) Render() (Box, error) {
}
// add padding
- if opt.Vertical {
+ if vertical {
width = maxTextWidth + textOffset + legendWidth
height = builtInSpacing * len(opt.Data)
} else {
@@ -169,27 +167,27 @@ func (l *legendPainter) Render() (Box, error) {
x0 := startX
y0 := y
- var drawIcon func(top, left int) int
+ var drawIcon func(top, left int, color Color) int
if opt.Icon == IconRect {
- drawIcon = func(top, left int) int {
- p.Rect(Box{
+ drawIcon = func(top, left int, color Color) int {
+ p.filledRect(Box{
Top: top - legendHeight + 8,
Left: left,
Right: left + legendWidth,
Bottom: top + 1,
IsSet: true,
- })
+ }, color, color, 0)
return left + legendWidth
}
} else {
- drawIcon = func(top, left int) int {
- p.LegendLineDot(Box{
+ drawIcon = func(top, left int, color Color) int {
+ p.legendLineDot(Box{
Top: top + 1,
Left: left,
Right: left + legendWidth,
Bottom: top + legendHeight + 1,
IsSet: true,
- })
+ }, color, 3, color)
return left + legendWidth
}
}
@@ -197,11 +195,7 @@ func (l *legendPainter) Render() (Box, error) {
lastIndex := len(opt.Data) - 1
for index, text := range opt.Data {
color := theme.GetSeriesColor(index)
- p.SetDrawingStyle(chartdraw.Style{
- FillColor: color,
- StrokeColor: color,
- })
- if opt.Vertical {
+ if vertical {
if opt.Align == AlignRight {
// adjust x0 so that the text will start with a right alignment to the longest line
x0 += maxTextWidth - measureList[index].Width()
@@ -218,7 +212,7 @@ func (l *legendPainter) Render() (Box, error) {
// recalculate width and center based off remaining width
var remainingWidth int
for i2 := index; i2 < len(opt.Data); i2++ {
- b := p.MeasureText(opt.Data[i2])
+ b := p.MeasureText(opt.Data[i2], 0, fontStyle)
remainingWidth += b.Width()
}
remainingCount := len(opt.Data) - index
@@ -237,17 +231,17 @@ func (l *legendPainter) Render() (Box, error) {
}
if opt.Align != AlignRight {
- x0 = drawIcon(y0, x0)
+ x0 = drawIcon(y0, x0, color)
x0 += textOffset
}
- p.Text(text, x0, y0)
+ p.Text(text, x0, y0, 0, fontStyle)
x0 += measureList[index].Width()
if opt.Align == AlignRight {
x0 += textOffset
- x0 = drawIcon(y0, x0)
+ x0 = drawIcon(y0, x0, color)
}
- if opt.Vertical {
+ if vertical {
y0 += builtInSpacing
x0 = startX
} else {
diff --git a/legend_test.go b/legend_test.go
index bd5e24a..b0345b9 100644
--- a/legend_test.go
+++ b/legend_test.go
@@ -21,7 +21,7 @@ func TestNewLegend(t *testing.T) {
{
name: "basic",
render: func(p *Painter) ([]byte, error) {
- _, err := NewLegendPainter(p, LegendOption{
+ _, err := newLegendPainter(p, LegendOption{
Data: []string{"One", "Two", "Three"},
}).Render()
if err != nil {
@@ -29,12 +29,12 @@ func TestNewLegend(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "position_left",
render: func(p *Painter) ([]byte, error) {
- _, err := NewLegendPainter(p, LegendOption{
+ _, err := newLegendPainter(p, LegendOption{
Data: []string{"One", "Two", "Three"},
Offset: OffsetLeft,
}).Render()
@@ -43,14 +43,14 @@ func TestNewLegend(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "position_vertical_with_rect",
render: func(p *Painter) ([]byte, error) {
- _, err := NewLegendPainter(p, LegendOption{
+ _, err := newLegendPainter(p, LegendOption{
Data: []string{"One", "Two", "Three"},
- Vertical: true,
+ Vertical: True(),
Icon: IconRect,
Offset: OffsetStr{
Left: "10%",
@@ -61,12 +61,12 @@ func TestNewLegend(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "custom_padding_and_font",
render: func(p *Painter) ([]byte, error) {
- _, err := NewLegendPainter(p, LegendOption{
+ _, err := newLegendPainter(p, LegendOption{
Data: []string{"One", "Two", "Three"},
FontStyle: FontStyle{
FontSize: 20.0,
@@ -79,12 +79,12 @@ func TestNewLegend(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "hidden",
render: func(p *Painter) ([]byte, error) {
- _, err := NewLegendPainter(p, LegendOption{
+ _, err := newLegendPainter(p, LegendOption{
Data: []string{"A", "B", "C"},
Show: False(),
}).Render()
@@ -98,7 +98,7 @@ func TestNewLegend(t *testing.T) {
{
name: "bottom_position",
render: func(p *Painter) ([]byte, error) {
- _, err := NewLegendPainter(p, LegendOption{
+ _, err := newLegendPainter(p, LegendOption{
Data: []string{"One", "Two Word", "Three Word Item", "Four Words Is Longer"},
Offset: OffsetStr{
Top: PositionBottom,
@@ -109,14 +109,14 @@ func TestNewLegend(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "vertical_right_position",
render: func(p *Painter) ([]byte, error) {
- _, err := NewLegendPainter(p, LegendOption{
+ _, err := newLegendPainter(p, LegendOption{
Data: []string{"One", "Two Word", "Three Word Item", "Four Words Is Longer"},
- Vertical: true,
+ Vertical: True(),
Offset: OffsetRight,
}).Render()
if err != nil {
@@ -124,14 +124,14 @@ func TestNewLegend(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "vertical_bottom_position",
render: func(p *Painter) ([]byte, error) {
- _, err := NewLegendPainter(p, LegendOption{
+ _, err := newLegendPainter(p, LegendOption{
Data: []string{"One", "Two Word", "Three Word Item", "Four Words Is Longer"},
- Vertical: true,
+ Vertical: True(),
Offset: OffsetStr{
Top: PositionBottom,
},
@@ -141,14 +141,14 @@ func TestNewLegend(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "vertical_right_bottom_position",
render: func(p *Painter) ([]byte, error) {
- _, err := NewLegendPainter(p, LegendOption{
+ _, err := newLegendPainter(p, LegendOption{
Data: []string{"One", "Two Word", "Three Word Item", "Four Words Is Longer"},
- Vertical: true,
+ Vertical: True(),
Offset: OffsetStr{
Left: PositionRight,
Top: PositionBottom,
@@ -159,14 +159,14 @@ func TestNewLegend(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "vertical_right_position_custom_font_size",
render: func(p *Painter) ([]byte, error) {
- _, err := NewLegendPainter(p, LegendOption{
+ _, err := newLegendPainter(p, LegendOption{
Data: []string{"One", "Two Word", "Three Word Item", "Four Words Is Longer"},
- Vertical: true,
+ Vertical: True(),
Offset: OffsetRight,
FontStyle: FontStyle{
FontSize: 6.0,
@@ -177,14 +177,14 @@ func TestNewLegend(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "vertical_right_position_with_padding",
render: func(p *Painter) ([]byte, error) {
- _, err := NewLegendPainter(p, LegendOption{
+ _, err := newLegendPainter(p, LegendOption{
Data: []string{"One", "Two Word", "Three Word Item", "Four Words Is Longer"},
- Vertical: true,
+ Vertical: True(),
Offset: OffsetRight,
Padding: Box{Top: 120, Left: 120, Right: 120, Bottom: 120},
}).Render()
@@ -193,12 +193,12 @@ func TestNewLegend(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "left_position_overflow",
render: func(p *Painter) ([]byte, error) {
- _, err := NewLegendPainter(p, LegendOption{
+ _, err := newLegendPainter(p, LegendOption{
Data: []string{"One", "Two Word", "Three Word Item", "Four Words Is Longer",
"Five Words Is Even Longer", "Six Words Is The Longest Tested"},
Offset: OffsetLeft,
@@ -208,12 +208,12 @@ func TestNewLegend(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "center_position_overflow",
render: func(p *Painter) ([]byte, error) {
- _, err := NewLegendPainter(p, LegendOption{
+ _, err := newLegendPainter(p, LegendOption{
Data: []string{"One", "Two Word", "Three Word Item", "Four Words Is Longer",
"Five Words Is Even Longer", "Six Words Is The Longest Tested"},
Offset: OffsetCenter,
@@ -223,12 +223,12 @@ func TestNewLegend(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "center_position_center_align_overflow",
render: func(p *Painter) ([]byte, error) {
- _, err := NewLegendPainter(p, LegendOption{
+ _, err := newLegendPainter(p, LegendOption{
Data: []string{"One", "Two Word", "Three Word Item", "Four Words Is Longer",
"Five Words Is Even Longer", "Six Words Is The Longest Tested"},
Offset: OffsetCenter,
@@ -239,12 +239,12 @@ func TestNewLegend(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "50%_position_overflow",
render: func(p *Painter) ([]byte, error) {
- _, err := NewLegendPainter(p, LegendOption{
+ _, err := newLegendPainter(p, LegendOption{
Data: []string{"One", "Two Word", "Three Word Item", "Four Words Is Longer",
"Five Words Is Even Longer", "Six Words Is The Longest Tested"},
Offset: OffsetStr{
@@ -256,15 +256,15 @@ func TestNewLegend(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "vertical_right_position_overflow",
render: func(p *Painter) ([]byte, error) {
- _, err := NewLegendPainter(p, LegendOption{
+ _, err := newLegendPainter(p, LegendOption{
Data: []string{"One", "Two Word", "Three Word Item", "Four Words Is Longer",
"Five Words Is Even Longer", "Six Words Is The Longest Tested"},
- Vertical: true,
+ Vertical: True(),
Offset: OffsetStr{
Left: "440",
},
@@ -274,12 +274,12 @@ func TestNewLegend(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "right_alignment",
render: func(p *Painter) ([]byte, error) {
- _, err := NewLegendPainter(p, LegendOption{
+ _, err := newLegendPainter(p, LegendOption{
Data: []string{"One", "Two Word", "Three Word Item", "Four Words Is Longer"},
Align: AlignRight,
}).Render()
@@ -288,14 +288,14 @@ func TestNewLegend(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "vertical_right_alignment",
render: func(p *Painter) ([]byte, error) {
- _, err := NewLegendPainter(p, LegendOption{
+ _, err := newLegendPainter(p, LegendOption{
Data: []string{"One", "Two Word", "Three Word Item", "Four Words Is Longer"},
- Vertical: true,
+ Vertical: True(),
Align: AlignRight,
}).Render()
if err != nil {
@@ -303,14 +303,14 @@ func TestNewLegend(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "vertical_right_alignment_left_position",
render: func(p *Painter) ([]byte, error) {
- _, err := NewLegendPainter(p, LegendOption{
+ _, err := newLegendPainter(p, LegendOption{
Data: []string{"One", "Two Word", "Three Word Item", "Four Words Is Longer"},
- Vertical: true,
+ Vertical: True(),
Offset: OffsetLeft,
Align: AlignRight,
}).Render()
@@ -319,17 +319,17 @@ func TestNewLegend(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
}
+
for i, tt := range tests {
t.Run(strconv.Itoa(i)+"-"+tt.name, func(t *testing.T) {
- p, err := NewPainter(PainterOptions{
+ p := NewPainter(PainterOptions{
OutputFormat: ChartOutputSVG,
Width: 600,
Height: 400,
}, PainterThemeOption(GetTheme(ThemeLight)))
- require.NoError(t, err)
data, err := tt.render(p)
require.NoError(t, err)
assertEqualSVG(t, tt.result, data)
diff --git a/line_chart.go b/line_chart.go
index 20f663c..6dbfe18 100644
--- a/line_chart.go
+++ b/line_chart.go
@@ -5,7 +5,6 @@ import (
"github.com/golang/freetype/truetype"
- "github.com/go-analyze/charts/chartdraw"
"github.com/go-analyze/charts/chartdraw/drawing"
)
@@ -14,14 +13,30 @@ type lineChart struct {
opt *LineChartOption
}
-// NewLineChart returns a line chart render
-func NewLineChart(p *Painter, opt LineChartOption) *lineChart {
+// newLineChart returns a line chart render
+func newLineChart(p *Painter, opt LineChartOption) *lineChart {
return &lineChart{
p: p,
opt: &opt,
}
}
+// NewLineChartOptionWithData returns an initialized LineChartOption with the SeriesList set for the provided data slice.
+func NewLineChartOptionWithData(data [][]float64) LineChartOption {
+ sl := NewSeriesListLine(data)
+ return LineChartOption{
+ SeriesList: sl,
+ Padding: defaultPadding,
+ Theme: GetDefaultTheme(),
+ Font: GetDefaultFont(),
+ XAxis: XAxisOption{
+ Data: make([]string, len(data[0])),
+ },
+ YAxis: make([]YAxisOption, sl.getYAxisCount()),
+ ValueFormatter: defaultValueFormatter,
+ }
+}
+
type LineChartOption struct {
// Theme specifies the colors used for the line chart.
Theme ColorPalette
@@ -41,9 +56,8 @@ type LineChartOption struct {
Legend LegendOption
// SymbolShow set this to *false or *true (using False() or True()) to force if the symbols should be shown or hidden.
SymbolShow *bool
- // TODO - rename to `LineStrokeWidth` to be more similar to ChartOptions?
- // StrokeWidth is the width of the rendered line.
- StrokeWidth float64
+ // LineStrokeWidth is the width of the rendered line.
+ LineStrokeWidth float64
// StrokeSmoothingTension should be between 0 and 1. At 0 perfectly straight lines will be used with 1 providing
// smoother lines. Because the tension smooths out the line, the line will no longer hit the data points exactly.
// The more variable the points, and the higher the tension, the more the line will be moved from the points.
@@ -52,6 +66,8 @@ type LineChartOption struct {
FillArea bool
// FillOpacity is the opacity (alpha) of the area fill.
FillOpacity uint8
+ // ValueFormatter defines how float values should be rendered to strings, notably for numeric axis labels.
+ ValueFormatter ValueFormatter
// backgroundIsFilled is set to true if the background is filled.
backgroundIsFilled bool
}
@@ -85,13 +101,13 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) (
} else {
xValues = xDivideValues
}
- markPointPainter := NewMarkPointPainter(seriesPainter)
- markLinePainter := NewMarkLinePainter(seriesPainter)
- rendererList := []Renderer{
+ markPointPainter := newMarkPointPainter(seriesPainter)
+ markLinePainter := newMarkLinePainter(seriesPainter)
+ rendererList := []renderer{
markPointPainter,
markLinePainter,
}
- strokeWidth := opt.StrokeWidth
+ strokeWidth := opt.LineStrokeWidth
if strokeWidth == 0 {
strokeWidth = defaultStrokeWidth
}
@@ -113,27 +129,17 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) (
for index := range seriesList {
series := seriesList[index]
seriesColor := opt.Theme.GetSeriesColor(series.index)
- drawingStyle := chartdraw.Style{
- StrokeColor: seriesColor,
- StrokeWidth: strokeWidth,
- }
yRange := result.axisRanges[series.YAxisIndex]
points := make([]Point, 0)
- var labelPainter *SeriesLabelPainter
- if series.Label.Show {
- labelPainter = NewSeriesLabelPainter(SeriesLabelPainterParams{
- P: seriesPainter,
- SeriesNames: seriesNames,
- Label: series.Label,
- Theme: opt.Theme,
- Font: opt.Font,
- })
+ var labelPainter *seriesLabelPainter
+ if flagIs(true, series.Label.Show) {
+ labelPainter = newSeriesLabelPainter(seriesPainter, seriesNames, series.Label, opt.Theme, opt.Font)
rendererList = append(rendererList, labelPainter)
}
for i, item := range series.Data {
- h := yRange.getRestHeight(item.Value)
- if item.Value == GetNullValue() {
+ h := yRange.getRestHeight(item)
+ if item == GetNullValue() {
h = math.MaxInt32
}
p := Point{
@@ -146,12 +152,12 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) (
if labelPainter == nil {
continue
}
- labelPainter.Add(LabelValue{
- Index: index,
- Value: item.Value,
- X: p.X,
- Y: p.Y,
- FontStyle: series.Label.FontStyle,
+ labelPainter.Add(labelValue{
+ index: index,
+ value: item,
+ x: p.X,
+ y: p.Y,
+ fontStyle: series.Label.FontStyle,
})
}
if opt.FillArea {
@@ -169,34 +175,28 @@ func (l *lineChart) render(result *defaultRenderResult, seriesList SeriesList) (
X: areaPoints[0].X,
Y: bottomY,
}, areaPoints[0])
- seriesPainter.SetDrawingStyle(chartdraw.Style{
- FillColor: seriesColor.WithAlpha(opacity),
- })
+ fillColor := seriesColor.WithAlpha(opacity)
if opt.StrokeSmoothingTension > 0 {
- seriesPainter.smoothFillChartArea(areaPoints, opt.StrokeSmoothingTension)
+ seriesPainter.smoothFillChartArea(areaPoints, opt.StrokeSmoothingTension, fillColor)
} else {
- seriesPainter.FillArea(areaPoints)
+ seriesPainter.FillArea(areaPoints, fillColor)
}
}
- seriesPainter.SetDrawingStyle(drawingStyle)
// draw line
if opt.StrokeSmoothingTension > 0 {
- seriesPainter.smoothLineStroke(points, opt.StrokeSmoothingTension)
+ seriesPainter.SmoothLineStroke(points, opt.StrokeSmoothingTension, seriesColor, strokeWidth)
} else {
- seriesPainter.LineStroke(points)
+ seriesPainter.LineStroke(points, seriesColor, strokeWidth)
}
// draw dots
- if opt.Theme.IsDark() {
- drawingStyle.FillColor = drawingStyle.StrokeColor
- } else {
- drawingStyle.FillColor = drawing.ColorWhite
- }
- drawingStyle.StrokeWidth = 1
- seriesPainter.SetDrawingStyle(drawingStyle)
if showSymbol {
- seriesPainter.Dots(points)
+ dotFillColor := drawing.ColorWhite
+ if opt.Theme.IsDark() {
+ dotFillColor = seriesColor
+ }
+ seriesPainter.Dots(points, dotFillColor, seriesColor, 1, 2)
}
markPointPainter.Add(markPointRenderOption{
FillColor: seriesColor,
@@ -234,13 +234,14 @@ func (l *lineChart) Render() (Box, error) {
}
renderResult, err := defaultRender(p, defaultRenderOption{
- Theme: opt.Theme,
- Padding: opt.Padding,
- SeriesList: opt.SeriesList,
- XAxis: opt.XAxis,
- YAxis: opt.YAxis,
- Title: opt.Title,
- Legend: opt.Legend,
+ theme: opt.Theme,
+ padding: opt.Padding,
+ seriesList: opt.SeriesList,
+ xAxis: &l.opt.XAxis,
+ yAxis: opt.YAxis,
+ title: opt.Title,
+ legend: &l.opt.Legend,
+ valueFormatter: opt.ValueFormatter,
backgroundIsFilled: opt.backgroundIsFilled,
})
if err != nil {
diff --git a/line_chart_test.go b/line_chart_test.go
index e38f5e3..5f1f193 100644
--- a/line_chart_test.go
+++ b/line_chart_test.go
@@ -4,6 +4,7 @@ import (
"strconv"
"testing"
+ "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/go-analyze/charts/chartdraw/drawing"
@@ -32,12 +33,13 @@ func makeFullLineChartOption() LineChartOption {
"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun",
},
},
+ YAxis: make([]YAxisOption, 1),
Legend: LegendOption{
Data: []string{
"Email", "Union Ads", "Video Ads", "Direct", "Search Engine",
},
},
- SeriesList: NewSeriesListDataFromValues(values),
+ SeriesList: NewSeriesListLine(values),
}
}
@@ -61,10 +63,11 @@ func makeBasicLineChartOption() LineChartOption {
"A", "B", "C", "D", "E", "F", "G",
},
},
+ YAxis: make([]YAxisOption, 1),
Legend: LegendOption{
Data: []string{"1", "2"},
},
- SeriesList: NewSeriesListDataFromValues(values),
+ SeriesList: NewSeriesListLine(values),
}
}
@@ -86,11 +89,29 @@ func makeMinimalLineChartOption() LineChartOption {
},
Show: False(),
},
+ YAxis: make([]YAxisOption, 1),
SymbolShow: False(),
- SeriesList: NewSeriesListDataFromValues(values),
+ SeriesList: NewSeriesListLine(values),
}
}
+func TestNewLineChartOptionWithData(t *testing.T) {
+ t.Parallel()
+
+ opt := NewLineChartOptionWithData([][]float64{
+ {12, 24},
+ {24, 48},
+ })
+
+ assert.Len(t, opt.SeriesList, 2)
+ assert.Equal(t, ChartTypeLine, opt.SeriesList[0].Type)
+ assert.Len(t, opt.YAxis, 1)
+ assert.Equal(t, defaultPadding, opt.Padding)
+
+ p := NewPainter(PainterOptions{})
+ assert.NoError(t, p.LineChart(opt))
+}
+
func TestLineChart(t *testing.T) {
t.Parallel()
@@ -104,13 +125,13 @@ func TestLineChart(t *testing.T) {
name: "basic_default",
defaultTheme: true,
makeOptions: makeFullLineChartOption,
- result: "",
+ result: "",
},
{
name: "basic_themed",
defaultTheme: false,
makeOptions: makeFullLineChartOption,
- result: "",
+ result: "",
},
{
name: "boundary_gap_disable",
@@ -120,7 +141,7 @@ func TestLineChart(t *testing.T) {
opt.XAxis.BoundaryGap = False()
return opt
},
- result: "",
+ result: "",
},
{
name: "boundary_gap_enable",
@@ -130,7 +151,7 @@ func TestLineChart(t *testing.T) {
opt.XAxis.BoundaryGap = True()
return opt
},
- result: "",
+ result: "",
},
{
name: "08Y_skip1",
@@ -145,7 +166,7 @@ func TestLineChart(t *testing.T) {
}
return opt
},
- result: "",
+ result: "",
},
{
name: "09Y_skip1",
@@ -160,7 +181,7 @@ func TestLineChart(t *testing.T) {
}
return opt
},
- result: "",
+ result: "",
},
{
name: "08Y_skip2",
@@ -175,7 +196,7 @@ func TestLineChart(t *testing.T) {
}
return opt
},
- result: "",
+ result: "",
},
{
name: "09Y_skip2",
@@ -190,7 +211,7 @@ func TestLineChart(t *testing.T) {
}
return opt
},
- result: "",
+ result: "",
},
{
name: "10Y_skip2",
@@ -205,7 +226,7 @@ func TestLineChart(t *testing.T) {
}
return opt
},
- result: "",
+ result: "",
},
{
name: "08Y_skip3",
@@ -220,7 +241,7 @@ func TestLineChart(t *testing.T) {
}
return opt
},
- result: "",
+ result: "",
},
{
name: "09Y_skip3",
@@ -235,7 +256,7 @@ func TestLineChart(t *testing.T) {
}
return opt
},
- result: "",
+ result: "",
},
{
name: "10Y_skip3",
@@ -250,7 +271,7 @@ func TestLineChart(t *testing.T) {
}
return opt
},
- result: "",
+ result: "",
},
{
name: "11Y_skip3",
@@ -265,7 +286,7 @@ func TestLineChart(t *testing.T) {
}
return opt
},
- result: "",
+ result: "",
},
{
name: "no_yaxis_split_line",
@@ -280,7 +301,7 @@ func TestLineChart(t *testing.T) {
}
return opt
},
- result: "",
+ result: "",
},
{
name: "yaxis_spine_line_show",
@@ -295,7 +316,7 @@ func TestLineChart(t *testing.T) {
}
return opt
},
- result: "",
+ result: "",
},
{
name: "zero_data",
@@ -306,10 +327,10 @@ func TestLineChart(t *testing.T) {
{0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0},
}
- opt.SeriesList = NewSeriesListDataFromValues(values)
+ opt.SeriesList = NewSeriesListLine(values)
return opt
},
- result: "",
+ result: "",
},
{
name: "tiny_range",
@@ -320,10 +341,10 @@ func TestLineChart(t *testing.T) {
{0.1, 0.2, 0.1, 0.2, 0.4, 0.2, 0.1},
{0.2, 0.4, 0.8, 0.4, 0.2, 0.1, 0.2},
}
- opt.SeriesList = NewSeriesListDataFromValues(values)
+ opt.SeriesList = NewSeriesListLine(values)
return opt
},
- result: "",
+ result: "",
},
{
name: "hidden_legend_and_x-axis",
@@ -334,7 +355,7 @@ func TestLineChart(t *testing.T) {
opt.XAxis.Show = False()
return opt
},
- result: "",
+ result: "",
},
{
name: "custom_font",
@@ -350,7 +371,7 @@ func TestLineChart(t *testing.T) {
opt.Title.FontStyle = customFont
return opt
},
- result: "",
+ result: "",
},
{
name: "title_offset_center_legend_right",
@@ -361,7 +382,7 @@ func TestLineChart(t *testing.T) {
opt.Legend.Offset = OffsetRight
return opt
},
- result: "",
+ result: "",
},
{
name: "title_offset_right",
@@ -371,7 +392,7 @@ func TestLineChart(t *testing.T) {
opt.Title.Offset = OffsetRight
return opt
},
- result: "",
+ result: "",
},
{
name: "title_offset_bottom_center",
@@ -384,7 +405,7 @@ func TestLineChart(t *testing.T) {
}
return opt
},
- result: "",
+ result: "",
},
{
name: "legend_offset_bottom",
@@ -396,7 +417,7 @@ func TestLineChart(t *testing.T) {
}
return opt
},
- result: "",
+ result: "",
},
{
name: "title_and_legend_offset_bottom",
@@ -411,18 +432,18 @@ func TestLineChart(t *testing.T) {
opt.Legend.Offset = bottomOffset
return opt
},
- result: "",
+ result: "",
},
{
name: "vertical_legend_offset_right",
defaultTheme: true,
makeOptions: func() LineChartOption {
opt := makeBasicLineChartOption()
- opt.Legend.Vertical = true
+ opt.Legend.Vertical = True()
opt.Legend.Offset = OffsetRight
return opt
},
- result: "",
+ result: "",
},
{
name: "legend_overlap_chart",
@@ -434,7 +455,7 @@ func TestLineChart(t *testing.T) {
opt.Legend.OverlayChart = True()
return opt
},
- result: "",
+ result: "",
},
{
name: "curved_line",
@@ -444,39 +465,39 @@ func TestLineChart(t *testing.T) {
opt.StrokeSmoothingTension = 0.8
return opt
},
- result: "",
+ result: "",
},
{
name: "line_gap",
defaultTheme: true,
makeOptions: func() LineChartOption {
opt := makeMinimalLineChartOption()
- opt.SeriesList[0].Data[3].Value = GetNullValue()
+ opt.SeriesList[0].Data[3] = GetNullValue()
return opt
},
- result: "",
+ result: "",
},
{
name: "line_gap_dot",
defaultTheme: true,
makeOptions: func() LineChartOption {
opt := makeMinimalLineChartOption()
- opt.SeriesList[0].Data[3].Value = GetNullValue()
- opt.SeriesList[0].Data[5].Value = GetNullValue()
+ opt.SeriesList[0].Data[3] = GetNullValue()
+ opt.SeriesList[0].Data[5] = GetNullValue()
return opt
},
- result: "",
+ result: "",
},
{
name: "line_gap_fill_area",
defaultTheme: true,
makeOptions: func() LineChartOption {
opt := makeMinimalLineChartOption()
- opt.SeriesList[0].Data[3].Value = GetNullValue()
+ opt.SeriesList[0].Data[3] = GetNullValue()
opt.FillArea = true
return opt
},
- result: "",
+ result: "",
},
{
name: "curved_line_gap",
@@ -484,10 +505,10 @@ func TestLineChart(t *testing.T) {
makeOptions: func() LineChartOption {
opt := makeMinimalLineChartOption()
opt.StrokeSmoothingTension = 0.8
- opt.SeriesList[0].Data[3].Value = GetNullValue()
+ opt.SeriesList[0].Data[3] = GetNullValue()
return opt
},
- result: "",
+ result: "",
},
{
name: "curved_line_gap_fill_area",
@@ -495,11 +516,11 @@ func TestLineChart(t *testing.T) {
makeOptions: func() LineChartOption {
opt := makeMinimalLineChartOption()
opt.StrokeSmoothingTension = 0.8
- opt.SeriesList[0].Data[3].Value = GetNullValue()
+ opt.SeriesList[0].Data[3] = GetNullValue()
opt.FillArea = true
return opt
},
- result: "",
+ result: "",
},
{
name: "fill_area",
@@ -510,7 +531,7 @@ func TestLineChart(t *testing.T) {
opt.FillOpacity = 100
return opt
},
- result: "",
+ result: "",
},
{
name: "fill_area_boundary_gap",
@@ -522,7 +543,7 @@ func TestLineChart(t *testing.T) {
opt.XAxis.BoundaryGap = True()
return opt
},
- result: "",
+ result: "",
},
{
name: "fill_area_curved_boundary_gap",
@@ -534,7 +555,7 @@ func TestLineChart(t *testing.T) {
opt.XAxis.BoundaryGap = True()
return opt
},
- result: "",
+ result: "",
},
{
name: "fill_area_curved_no_gap",
@@ -546,7 +567,19 @@ func TestLineChart(t *testing.T) {
opt.XAxis.BoundaryGap = False()
return opt
},
- result: "",
+ result: "",
+ },
+ {
+ name: "value_formatter",
+ defaultTheme: true,
+ makeOptions: func() LineChartOption {
+ opt := makeMinimalLineChartOption()
+ opt.YAxis[0].ValueFormatter = func(f float64) string {
+ return "f"
+ }
+ return opt
+ },
+ result: "",
},
}
@@ -558,21 +591,18 @@ func TestLineChart(t *testing.T) {
}
if tt.defaultTheme {
t.Run(strconv.Itoa(i)+"-"+tt.name, func(t *testing.T) {
- p, err := NewPainter(painterOptions)
- require.NoError(t, err)
+ p := NewPainter(painterOptions)
validateLineChartRender(t, p, tt.makeOptions(), tt.result)
})
} else {
t.Run(strconv.Itoa(i)+"-"+tt.name+"-theme_painter", func(t *testing.T) {
- p, err := NewPainter(painterOptions, PainterThemeOption(GetTheme(ThemeVividDark)))
- require.NoError(t, err)
+ p := NewPainter(painterOptions, PainterThemeOption(GetTheme(ThemeVividDark)))
validateLineChartRender(t, p, tt.makeOptions(), tt.result)
})
t.Run(strconv.Itoa(i)+"-"+tt.name+"-theme_opt", func(t *testing.T) {
- p, err := NewPainter(painterOptions)
- require.NoError(t, err)
+ p := NewPainter(painterOptions)
opt := tt.makeOptions()
opt.Theme = GetTheme(ThemeVividDark)
@@ -585,7 +615,7 @@ func TestLineChart(t *testing.T) {
func validateLineChartRender(t *testing.T, p *Painter, opt LineChartOption, expectedResult string) {
t.Helper()
- _, err := NewLineChart(p, opt).Render()
+ err := p.LineChart(opt)
require.NoError(t, err)
data, err := p.Bytes()
require.NoError(t, err)
diff --git a/mark_line.go b/mark_line.go
index 2b42189..fb0d226 100644
--- a/mark_line.go
+++ b/mark_line.go
@@ -2,8 +2,6 @@ package charts
import (
"github.com/golang/freetype/truetype"
-
- "github.com/go-analyze/charts/chartdraw"
)
// NewMarkLine returns a series mark line
@@ -28,8 +26,8 @@ func (m *markLinePainter) Add(opt markLineRenderOption) {
m.options = append(m.options, opt)
}
-// NewMarkLinePainter returns a mark line renderer
-func NewMarkLinePainter(p *Painter) *markLinePainter {
+// newMarkLinePainter returns a mark line renderer
+func newMarkLinePainter(p *Painter) *markLinePainter {
return &markLinePainter{
p: p,
options: make([]markLineRenderOption, 0),
@@ -52,41 +50,28 @@ func (m *markLinePainter) Render() (Box, error) {
if len(s.MarkLine.Data) == 0 {
continue
}
- font := opt.Font
- if font == nil {
- font = GetDefaultFont()
- }
summary := s.Summary()
+ fontStyle := FontStyle{
+ Font: getPreferredFont(opt.Font),
+ FontColor: opt.FontColor,
+ FontSize: labelFontSize,
+ }
for _, markLine := range s.MarkLine.Data {
- // since the mark line will modify the style, it must be reset every time
- painter.OverrideDrawingStyle(chartdraw.Style{
- FillColor: opt.FillColor,
- StrokeColor: opt.StrokeColor,
- StrokeWidth: 1,
- StrokeDashArray: []float64{
- 4,
- 2,
- },
- }).OverrideFontStyle(FontStyle{
- Font: font,
- FontColor: opt.FontColor,
- FontSize: labelFontSize,
- })
value := float64(0)
switch markLine.Type {
case SeriesMarkDataTypeMax:
- value = summary.MaxValue
+ value = summary.Max
case SeriesMarkDataTypeMin:
- value = summary.MinValue
+ value = summary.Min
default:
- value = summary.AverageValue
+ value = summary.Average
}
y := opt.Range.getRestHeight(value)
width := painter.Width()
text := defaultValueFormatter(value)
- textBox := painter.MeasureText(text)
- painter.MarkLine(0, y, width-2)
- painter.Text(text, width, y+textBox.Height()>>1-2)
+ textBox := painter.MeasureText(text, 0, fontStyle)
+ painter.MarkLine(0, y, width-2, opt.FillColor, opt.StrokeColor, 1, []float64{4, 2})
+ painter.Text(text, width, y+textBox.Height()>>1-2, 0, fontStyle)
}
}
return BoxZero, nil
diff --git a/mark_line_test.go b/mark_line_test.go
index 75fb9f3..8ca6efc 100644
--- a/mark_line_test.go
+++ b/mark_line_test.go
@@ -18,8 +18,10 @@ func TestMarkLine(t *testing.T) {
}{
{
render: func(p *Painter) ([]byte, error) {
- markLine := NewMarkLinePainter(p)
- series := NewSeriesFromValues([]float64{1, 2, 3})
+ markLine := newMarkLinePainter(p)
+ series := Series{
+ Data: []float64{1, 2, 3},
+ }
series.MarkLine = NewMarkLine(
SeriesMarkDataTypeMax,
SeriesMarkDataTypeAverage,
@@ -30,24 +32,25 @@ func TestMarkLine(t *testing.T) {
FontColor: drawing.ColorBlack,
StrokeColor: drawing.ColorBlack,
Series: series,
- Range: NewRange(p, p.Height(), 6, 0.0, 5.0, 0.0, 0.0),
+ Range: newRange(p, nil,
+ p.Height(), 6, 0.0, 5.0, 0.0, 0.0),
})
if _, err := markLine.Render(); err != nil {
return nil, err
}
return p.Bytes()
},
- result: "",
+ result: "",
},
}
+
for i, tt := range tests {
t.Run(strconv.Itoa(i), func(t *testing.T) {
- p, err := NewPainter(PainterOptions{
+ p := NewPainter(PainterOptions{
OutputFormat: ChartOutputSVG,
Width: 600,
Height: 400,
}, PainterThemeOption(GetTheme(ThemeLight)))
- require.NoError(t, err)
data, err := tt.render(p.Child(PainterPaddingOption(Box{
Left: 20,
Top: 20,
diff --git a/mark_point.go b/mark_point.go
index 27e3f84..a6ee134 100644
--- a/mark_point.go
+++ b/mark_point.go
@@ -35,8 +35,8 @@ type markPointRenderOption struct {
Points []Point
}
-// NewMarkPointPainter returns a mark point renderer
-func NewMarkPointPainter(p *Painter) *markPointPainter {
+// newMarkPointPainter returns a mark point renderer
+func newMarkPointPainter(p *Painter) *markPointPainter {
return &markPointPainter{
p: p,
options: make([]markPointRenderOption, 0),
@@ -67,29 +67,24 @@ func (m *markPointPainter) Render() (Box, error) {
} else {
textStyle.FontColor = defaultDarkFontColor
}
- painter.OverrideDrawingStyle(chartdraw.Style{
- FillColor: opt.FillColor,
- }).OverrideFontStyle(textStyle.FontStyle)
for _, markPointData := range opt.Series.MarkPoint.Data {
textStyle.FontSize = labelFontSize
- painter.OverrideFontStyle(textStyle.FontStyle)
p := points[summary.MinIndex]
- value := summary.MinValue
+ value := summary.Min
switch markPointData.Type {
case SeriesMarkDataTypeMax:
p = points[summary.MaxIndex]
- value = summary.MaxValue
+ value = summary.Max
}
- painter.Pin(p.X, p.Y-symbolSize>>1, symbolSize)
+ painter.Pin(p.X, p.Y-symbolSize>>1, symbolSize, opt.FillColor, opt.FillColor, 0.0)
text := defaultValueFormatter(value)
- textBox := painter.MeasureText(text)
+ textBox := painter.MeasureText(text, 0, textStyle.FontStyle)
if textBox.Width() > symbolSize {
textStyle.FontSize = smallLabelFontSize
- painter.OverrideFontStyle(textStyle.FontStyle)
- textBox = painter.MeasureText(text)
+ textBox = painter.MeasureText(text, 0, textStyle.FontStyle)
}
- painter.Text(text, p.X-textBox.Width()>>1, p.Y-symbolSize>>1-2)
+ painter.Text(text, p.X-textBox.Width()>>1, p.Y-symbolSize>>1-2, 0, textStyle.FontStyle)
}
}
return BoxZero, nil
diff --git a/mark_point_test.go b/mark_point_test.go
index 685466b..f8a74ac 100644
--- a/mark_point_test.go
+++ b/mark_point_test.go
@@ -18,9 +18,11 @@ func TestMarkPoint(t *testing.T) {
}{
{
render: func(p *Painter) ([]byte, error) {
- series := NewSeriesFromValues([]float64{1, 2, 3})
+ series := Series{
+ Data: []float64{1, 2, 3},
+ }
series.MarkPoint = NewMarkPoint(SeriesMarkDataTypeMax)
- markPoint := NewMarkPointPainter(p)
+ markPoint := newMarkPointPainter(p)
markPoint.Add(markPointRenderOption{
FillColor: drawing.ColorBlack,
Series: series,
@@ -35,18 +37,17 @@ func TestMarkPoint(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
}
for i, tt := range tests {
t.Run(strconv.Itoa(i), func(t *testing.T) {
- p, err := NewPainter(PainterOptions{
+ p := NewPainter(PainterOptions{
OutputFormat: ChartOutputSVG,
Width: 600,
Height: 400,
}, PainterThemeOption(GetTheme(ThemeLight)))
- require.NoError(t, err)
data, err := tt.render(p.Child(PainterPaddingOption(Box{
Left: 20,
Top: 20,
diff --git a/painter.go b/painter.go
index e1c11ee..d177908 100644
--- a/painter.go
+++ b/painter.go
@@ -16,15 +16,22 @@ var defaultValueFormatter = func(val float64) string {
return FormatValueHumanizeShort(val, 2, false)
}
+func getPreferredValueFormatter(t ...ValueFormatter) ValueFormatter {
+ for _, vf := range t {
+ if vf != nil {
+ return vf
+ }
+ }
+ return defaultValueFormatter
+}
+
// Painter is the primary struct for drawing charts/graphs.
type Painter struct {
- render chartdraw.Renderer
- box Box
- style chartdraw.Style
- theme ColorPalette
- font *truetype.Font
- outputFormat string
- valueFormatter ValueFormatter
+ render chartdraw.Renderer
+ outputFormat string
+ box Box
+ theme ColorPalette
+ font *truetype.Font
}
// PainterOptions contains parameters for creating a new Painter.
@@ -35,51 +42,42 @@ type PainterOptions struct {
Width int
// Height is the height of the draw painter.
Height int
- // Font is the font used for rendering text.
+ // Font is the default font used for rendering text.
Font *truetype.Font
-}
-
-type PainterOption func(*Painter)
-
-type TicksOption struct {
- // the first tick index
- First int
- Length int
- Vertical bool
- LabelCount int
- TickCount int
- TickSpaces int
-}
-
-type MultiTextOption struct {
- TextList []string
- Vertical bool
- CenterLabels bool
- Align string
- TextRotation float64
- Offset OffsetInt
- // The first text index
- First int
- LabelCount int
- TickCount int
- LabelSkipCount int
-}
-
-type GridOption struct {
- // Columns is the count of columns in the grid.
- Columns int
- // Rows are the count of rows in the grid.
- Rows int
- // ColumnSpans specifies the span for each column.
- ColumnSpans []int
- // IgnoreColumnLines specifies index for columns to not display.
- IgnoreColumnLines []int
- // IgnoreRowLines specifies index for rows to not display.
- IgnoreRowLines []int
-}
-
-// PainterPaddingOption sets the padding of draw painter
-func PainterPaddingOption(padding Box) PainterOption {
+ // Theme is the default theme to be used if the chart does not specify a theme.
+ Theme ColorPalette
+}
+
+// PainterOptionFunc defines a function that can modify a Painter after creation.
+type PainterOptionFunc func(*Painter)
+
+type ticksOption struct {
+ firstIndex int
+ length int
+ vertical bool
+ labelCount int
+ tickCount int
+ tickSpaces int
+ strokeWidth float64
+ strokeColor Color
+}
+
+type multiTextOption struct {
+ textList []string
+ fontStyle FontStyle
+ vertical bool
+ centerLabels bool
+ align string
+ textRotation float64
+ offset OffsetInt
+ firstIndex int
+ labelCount int
+ tickCount int
+ labelSkipCount int
+}
+
+// PainterPaddingOption sets the padding of the draw painter.
+func PainterPaddingOption(padding Box) PainterOptionFunc {
return func(p *Painter) {
p.box.Left += padding.Left
p.box.Top += padding.Top
@@ -89,7 +87,7 @@ func PainterPaddingOption(padding Box) PainterOption {
}
// PainterBoxOption sets a specific box for the Painter to draw within.
-func PainterBoxOption(box Box) PainterOption {
+func PainterBoxOption(box Box) PainterOptionFunc {
return func(p *Painter) {
if box.IsZero() {
return
@@ -98,181 +96,69 @@ func PainterBoxOption(box Box) PainterOption {
}
}
-// PainterFontOption sets the font of draw painter
-func PainterFontOption(font *truetype.Font) PainterOption {
- return func(p *Painter) {
- if font == nil {
- return
- }
- p.font = font
- }
-}
-
-// PainterStyleOption sets the style of draw painter
-func PainterStyleOption(style chartdraw.Style) PainterOption {
- return func(p *Painter) {
- p.SetStyle(style)
- }
-}
-
// PainterThemeOption sets a color palette theme default for the Painter.
// This theme is used if the specific chart options don't have a theme set.
-func PainterThemeOption(theme ColorPalette) PainterOption {
+func PainterThemeOption(theme ColorPalette) PainterOptionFunc {
return func(p *Painter) {
- if theme == nil {
- return
- }
- p.theme = theme
+ p.theme = getPreferredTheme(theme)
}
}
-// PainterWidthHeightOption set width or height of draw painter
-func PainterWidthHeightOption(width, height int) PainterOption {
+// PainterFontOption sets the default font face for the Painter.
+// This font is used if the FontStyle specified in chart configs does not specify another face.
+func PainterFontOption(font *truetype.Font) PainterOptionFunc {
return func(p *Painter) {
- if width > 0 {
- p.box.Right = p.box.Left + width
- }
- if height > 0 {
- p.box.Bottom = p.box.Top + height
- }
+ p.font = getPreferredFont(font)
}
}
-// TODO - try to remove the error return
-// NewPainter creates a painter
-func NewPainter(opts PainterOptions, opt ...PainterOption) (*Painter, error) {
+// NewPainter creates a painter which can be used to render charts to (using for example newLineChart).
+func NewPainter(opts PainterOptions, opt ...PainterOptionFunc) *Painter {
if opts.Width <= 0 {
opts.Width = defaultChartWidth
}
if opts.Height <= 0 {
opts.Height = defaultChartHeight
}
- if opts.Font == nil {
- opts.Font = GetDefaultFont()
- }
fn := chartdraw.PNG
if opts.OutputFormat == ChartOutputSVG {
fn = chartdraw.SVG
}
- width := opts.Width
- height := opts.Height
- r, err := fn(width, height)
- if err != nil {
- return nil, err
- }
- r.SetFont(opts.Font)
p := &Painter{
- render: r,
+ outputFormat: opts.OutputFormat,
+ render: fn(opts.Width, opts.Height),
box: Box{
Right: opts.Width,
Bottom: opts.Height,
IsSet: true,
},
- font: opts.Font,
- outputFormat: opts.OutputFormat,
+ font: opts.Font,
+ theme: opts.Theme,
}
p.setOptions(opt...)
- if p.theme == nil {
- p.theme = GetDefaultTheme()
- }
- return p, nil
+ return p
}
-func (p *Painter) setOptions(opts ...PainterOption) {
+
+func (p *Painter) setOptions(opts ...PainterOptionFunc) {
for _, fn := range opts {
fn(p)
}
}
-func (p *Painter) Child(opt ...PainterOption) *Painter {
+// Child returns a painter with the passed-in options applied to it. Useful when you want to render relative to only a portion of the canvas via PainterBoxOption.
+func (p *Painter) Child(opt ...PainterOptionFunc) *Painter {
child := &Painter{
- render: p.render,
- box: p.box.Clone(),
- style: p.style,
- theme: p.theme,
- font: p.font,
- outputFormat: p.outputFormat,
- valueFormatter: p.valueFormatter,
+ render: p.render,
+ box: p.box.Clone(),
+ theme: p.theme,
+ font: p.font,
}
child.setOptions(opt...)
return child
}
-func (p *Painter) SetStyle(style chartdraw.Style) {
- if style.Font == nil {
- style.Font = p.font
- }
- p.style = style
- style.WriteToRenderer(p.render)
-}
-
-func overrideStyle(defaultStyle chartdraw.Style, style chartdraw.Style) chartdraw.Style {
- if style.StrokeWidth == 0 {
- style.StrokeWidth = defaultStyle.StrokeWidth
- }
- if style.StrokeColor.IsZero() {
- style.StrokeColor = defaultStyle.StrokeColor
- }
- if style.StrokeDashArray == nil {
- style.StrokeDashArray = defaultStyle.StrokeDashArray
- }
- if style.DotColor.IsZero() {
- style.DotColor = defaultStyle.DotColor
- }
- if style.DotWidth == 0 {
- style.DotWidth = defaultStyle.DotWidth
- }
- if style.FillColor.IsZero() {
- style.FillColor = defaultStyle.FillColor
- }
- if style.FontSize == 0 {
- style.FontSize = defaultStyle.FontSize
- }
- if style.FontColor.IsZero() {
- style.FontColor = defaultStyle.FontColor
- }
- if style.Font == nil {
- style.Font = defaultStyle.Font
- }
- return style
-}
-
-func (p *Painter) OverrideDrawingStyle(style chartdraw.Style) *Painter {
- s := overrideStyle(p.style, style)
- p.SetDrawingStyle(s)
- return p
-}
-
-func (p *Painter) SetDrawingStyle(style chartdraw.Style) *Painter {
- style.WriteDrawingOptionsToRenderer(p.render)
- return p
-}
-
-func (p *Painter) SetFontStyle(style chartdraw.FontStyle) *Painter {
- if style.Font == nil {
- style.Font = p.font
- }
- if style.FontColor.IsZero() {
- style.FontColor = p.style.FontColor
- }
- if style.FontSize == 0 {
- style.FontSize = p.style.FontSize
- }
- style.WriteTextOptionsToRenderer(p.render)
- return p
-}
-func (p *Painter) OverrideFontStyle(style chartdraw.FontStyle) *Painter {
- s := overrideStyle(p.style, chartdraw.Style{FontStyle: style})
- p.SetFontStyle(s.FontStyle)
- return p
-}
-
-func (p *Painter) ResetStyle() *Painter {
- p.style.WriteToRenderer(p.render)
- return p
-}
-
-// Bytes returns the data of draw canvas
+// Bytes returns the final rendered data as a byte slice.
func (p *Painter) Bytes() ([]byte, error) {
buffer := bytes.Buffer{}
if err := p.render.Save(&buffer); err != nil {
@@ -281,137 +167,53 @@ func (p *Painter) Bytes() ([]byte, error) {
return buffer.Bytes(), nil
}
-// MoveTo moves the cursor to a given point
-func (p *Painter) MoveTo(x, y int) *Painter {
+// moveTo sets the current path cursor to a given point.
+func (p *Painter) moveTo(x, y int) {
p.render.MoveTo(x+p.box.Left, y+p.box.Top)
- return p
}
-func (p *Painter) ArcTo(cx, cy int, rx, ry, startAngle, delta float64) *Painter {
+// arcTo renders an arc from the current cursor.
+func (p *Painter) arcTo(cx, cy int, rx, ry, startAngle, delta float64) {
p.render.ArcTo(cx+p.box.Left, cy+p.box.Top, rx, ry, startAngle, delta)
- return p
-}
-
-func (p *Painter) LineTo(x, y int) *Painter {
- p.render.LineTo(x+p.box.Left, y+p.box.Top)
- return p
}
-func (p *Painter) QuadCurveTo(cx, cy, x, y int) *Painter {
+// quadCurveTo draws a quadratic curve from the current cursor using a control point (cx, cy) and ending at (x, y).
+func (p *Painter) quadCurveTo(cx, cy, x, y int) {
p.render.QuadCurveTo(cx+p.box.Left, cy+p.box.Top, x+p.box.Left, y+p.box.Top)
- return p
-}
-
-func (p *Painter) Pin(x, y, width int) *Painter {
- r := float64(width) / 2
- y -= width / 4
- angle := chartdraw.DegreesToRadians(15)
- box := p.box
-
- startAngle := math.Pi/2 + angle
- delta := 2*math.Pi - 2*angle
- p.ArcTo(x, y, r, r, startAngle, delta)
- p.LineTo(x, y)
- p.Close()
- p.FillStroke()
-
- startX := x - int(r)
- startY := y
- endX := x + int(r)
- endY := y
- p.MoveTo(startX, startY)
-
- left := box.Left
- top := box.Top
- cx := x
- cy := y + int(r*2.5)
- p.render.QuadCurveTo(cx+left, cy+top, endX+left, endY+top)
- p.Close()
- p.Fill()
- return p
}
-func (p *Painter) arrow(x, y, width, height int, direction string) *Painter {
- halfWidth := width >> 1
- halfHeight := height >> 1
- if direction == PositionTop || direction == PositionBottom {
- x0 := x - halfWidth
- x1 := x0 + width
- dy := -height / 3
- y0 := y
- y1 := y0 - height
- if direction == PositionBottom {
- y0 = y - height
- y1 = y
- dy = 2 * dy
- }
- p.MoveTo(x0, y0)
- p.LineTo(x0+halfWidth, y1)
- p.LineTo(x1, y0)
- p.LineTo(x0+halfWidth, y+dy)
- p.LineTo(x0, y0)
- } else {
- x0 := x + width
- x1 := x0 - width
- y0 := y - halfHeight
- dx := -width / 3
- if direction == PositionRight {
- x0 = x - width
- dx = -dx
- x1 = x0 + width
- }
- p.MoveTo(x0, y0)
- p.LineTo(x1, y0+halfHeight)
- p.LineTo(x0, y0+height)
- p.LineTo(x0+dx, y0+halfHeight)
- p.LineTo(x0, y0)
- }
- p.FillStroke()
- return p
-}
-
-func (p *Painter) ArrowLeft(x, y, width, height int) *Painter {
- p.arrow(x, y, width, height, PositionLeft)
- return p
-}
-
-func (p *Painter) ArrowRight(x, y, width, height int) *Painter {
- p.arrow(x, y, width, height, PositionRight)
- return p
-}
-
-func (p *Painter) ArrowTop(x, y, width, height int) *Painter {
- p.arrow(x, y, width, height, PositionTop)
- return p
-}
-func (p *Painter) ArrowBottom(x, y, width, height int) *Painter {
- p.arrow(x, y, width, height, PositionBottom)
- return p
+// lineTo draws a line from the current path cursor to the given point.
+func (p *Painter) lineTo(x, y int) {
+ p.render.LineTo(x+p.box.Left, y+p.box.Top)
}
-func (p *Painter) Circle(radius float64, x, y int) *Painter {
- p.render.Circle(radius, x+p.box.Left, y+p.box.Top)
- return p
+// close finalizes a shape as drawn by the current path.
+func (p *Painter) close() {
+ p.render.Close()
}
-func (p *Painter) Stroke() *Painter {
+// stroke performs a stroke using the provided color and width, then resets style.
+func (p *Painter) stroke(strokeColor Color, strokeWidth float64) {
+ defer p.render.ResetStyle()
+ p.render.SetStrokeColor(strokeColor)
+ p.render.SetStrokeWidth(strokeWidth)
p.render.Stroke()
- return p
}
-func (p *Painter) Close() *Painter {
- p.render.Close()
- return p
+// fill performs a fill with the given color, then resets style.
+func (p *Painter) fill(fillColor Color) {
+ defer p.render.ResetStyle()
+ p.render.SetFillColor(fillColor)
+ p.render.Fill()
}
-func (p *Painter) FillStroke() *Painter {
+// fillStroke performs a fill+stroke with the given colors and stroke width, then resets style.
+func (p *Painter) fillStroke(fillColor, strokeColor Color, strokeWidth float64) {
+ defer p.render.ResetStyle()
+ p.render.SetFillColor(fillColor)
+ p.render.SetStrokeColor(strokeColor)
+ p.render.SetStrokeWidth(strokeWidth)
p.render.FillStroke()
- return p
-}
-
-func (p *Painter) Fill() *Painter {
- p.render.Fill()
- return p
}
// Width returns the drawable width of the painter's box.
@@ -425,15 +227,33 @@ func (p *Painter) Height() int {
}
// MeasureText will provide the rendered size of the text for the provided font style.
-func (p *Painter) MeasureText(text string) Box {
- return p.render.MeasureText(text)
+func (p *Painter) MeasureText(text string, textRotation float64, fontStyle FontStyle) Box {
+ if fontStyle.Font == nil {
+ fontStyle.Font = getPreferredFont(p.font)
+ }
+ if fontStyle.Font == nil || fontStyle.FontSize == 0 || fontStyle.FontColor.IsTransparent() {
+ return BoxZero
+ }
+ if textRotation != 0 {
+ defer p.render.ClearTextRotation()
+ p.render.SetTextRotation(textRotation)
+ }
+ defer p.render.ResetStyle()
+ p.render.SetFont(fontStyle.Font)
+ p.render.SetFontSize(fontStyle.FontSize)
+ p.render.SetFontColor(fontStyle.FontColor)
+ box := p.render.MeasureText(text)
+ return box
}
-func (p *Painter) MeasureTextMaxWidthHeight(textList []string) (int, int) {
+func (p *Painter) measureTextMaxWidthHeight(textList []string, textRotation float64, fontStyle FontStyle) (int, int) {
+ if fontStyle.Font == nil {
+ fontStyle.Font = getPreferredFont(p.font)
+ }
maxWidth := 0
maxHeight := 0
for _, text := range textList {
- box := p.MeasureText(text)
+ box := p.MeasureText(text, textRotation, fontStyle)
if maxWidth < box.Width() {
maxWidth = box.Width()
}
@@ -444,25 +264,36 @@ func (p *Painter) MeasureTextMaxWidthHeight(textList []string) (int, int) {
return maxWidth, maxHeight
}
+// Circle draws a circle at the given coords with a given radius.
+func (p *Painter) Circle(radius float64, x, y int, fillColor, strokeColor Color, strokeWidth float64) {
+ // This function has a slight behavior difference between png and svg.
+ // We need to set the style attributes before the `Circle` call for SVG.
+ defer p.render.ResetStyle()
+ p.render.SetFillColor(fillColor)
+ p.render.SetStrokeColor(strokeColor)
+ p.render.SetStrokeWidth(strokeWidth)
+ p.render.Circle(radius, x+p.box.Left, y+p.box.Top)
+ p.render.FillStroke()
+}
+
// LineStroke draws a line in the graph from point to point with the specified stroke color/width.
// Points with values of math.MaxInt32 will be skipped, resulting in a gap.
// Single or isolated points will result in just a dot being drawn at the point.
-func (p *Painter) LineStroke(points []Point) *Painter {
+func (p *Painter) LineStroke(points []Point, strokeColor Color, strokeWidth float64) {
var valid []Point
for _, pt := range points {
if pt.Y == math.MaxInt32 {
// If we encounter a break, draw the accumulated segment
p.drawStraightPath(valid, true)
- p.Stroke()
+ p.stroke(strokeColor, strokeWidth)
valid = valid[:0] // reset
continue
}
valid = append(valid, pt)
}
-
- // Draw the last segment if there is one
+ // Draw the last segment
p.drawStraightPath(valid, true)
- return p.Stroke()
+ p.stroke(strokeColor, strokeWidth)
}
// drawStraightPath draws a simple (non-curved) path for the given points.
@@ -473,22 +304,24 @@ func (p *Painter) drawStraightPath(points []Point, dotForSinglePoint bool) {
return
} else if pointCount == 1 {
if dotForSinglePoint {
- p.Dots(points)
+ p.render.Circle(2.0, points[0].X+p.box.Left, points[0].Y+p.box.Top)
}
+ return
}
- p.MoveTo(points[0].X, points[0].Y)
+ p.moveTo(points[0].X, points[0].Y)
for i := 1; i < pointCount; i++ {
- p.LineTo(points[i].X, points[i].Y)
+ p.lineTo(points[i].X, points[i].Y)
}
}
-// smoothLineStroke draws a smooth curve through the given points using Quadratic Bézier segments and a
+// SmoothLineStroke draws a smooth curve through the given points using Quadratic Bézier segments and a
// `tension` parameter in [0..1] with 0 providing straight lines between midpoints and 1 providing a smoother line.
// Because the tension smooths out the line, the line will no longer hit the provided points exactly. The more variable
-// the points, and the higher the tension, the more the line will be
-func (p *Painter) smoothLineStroke(points []Point, tension float64) *Painter {
+// the points, and the higher the tension, the more the line will be.
+func (p *Painter) SmoothLineStroke(points []Point, tension float64, strokeColor Color, strokeWidth float64) {
if tension <= 0 {
- return p.LineStroke(points)
+ p.LineStroke(points, strokeColor, strokeWidth)
+ return
} else if tension > 1 {
tension = 1
}
@@ -498,28 +331,26 @@ func (p *Painter) smoothLineStroke(points []Point, tension float64) *Painter {
if pt.Y == math.MaxInt32 {
// When a line break is found, draw the curve for the accumulated valid points if any
p.drawSmoothCurve(valid, tension, true)
- p.Stroke()
+ p.stroke(strokeColor, strokeWidth)
valid = valid[:0] // reset
continue
}
-
valid = append(valid, pt)
}
// draw any remaining points collected
p.drawSmoothCurve(valid, tension, true)
- return p.Stroke()
+ p.stroke(strokeColor, strokeWidth)
}
// drawSmoothCurve handles the actual path drawing (MoveTo/LineTo/QuadCurveTo)
-// but does NOT call Stroke() or Fill(). This allows us to reuse this path
-// logic for either smoothLineStroke or smoothFillArea.
+// but does NOT call Stroke() or Fill(), letting caller do it.
func (p *Painter) drawSmoothCurve(points []Point, tension float64, dotForSinglePoint bool) {
if len(points) < 3 { // Not enough points to form a curve, draw a line
p.drawStraightPath(points, dotForSinglePoint)
return
}
- p.MoveTo(points[0].X, points[0].Y) // Start from the first valid point
+ p.moveTo(points[0].X, points[0].Y) // Start from the first valid point
// Handle each segment between points with quadratic Bézier curves
for i := 1; i < len(points)-1; i++ {
@@ -532,96 +363,156 @@ func (p *Painter) drawSmoothCurve(points []Point, tension float64, dotForSingleP
cx := float64(x1) + tension*(mx-float64(x1))
cy := float64(y1) + tension*(my-float64(y1))
- p.QuadCurveTo(x1, y1, int(cx), int(cy))
+ p.quadCurveTo(x1, y1, int(cx), int(cy))
}
// Connect the second-to-last point to the last point
n := len(points)
- p.QuadCurveTo(points[n-2].X, points[n-2].Y, points[n-1].X, points[n-1].Y)
-}
-
-// SmoothLineStroke is Deprecated. This implementation produced sharp joints at the point, and will be removed in v0.4.0.
-func (p *Painter) SmoothLineStroke(points []Point) *Painter {
- prevX := 0
- prevY := 0
- for index, point := range points {
- x := point.X
- y := point.Y
- if index == 0 {
- p.MoveTo(x, y)
- } else {
- cx := prevX + (x-prevX)/5
- cy := y + (y-prevY)/2
- p.QuadCurveTo(cx, cy, x, y)
- }
- prevX = x
- prevY = y
- }
- p.Stroke()
- return p
+ p.quadCurveTo(points[n-2].X, points[n-2].Y, points[n-1].X, points[n-1].Y)
}
-func (p *Painter) SetBackground(width, height int, color Color, inside ...bool) *Painter {
- r := p.render
- s := chartdraw.Style{
- FillColor: color,
- }
- // background color
- p.SetDrawingStyle(s)
- defer p.ResetStyle()
- if len(inside) != 0 && inside[0] {
- p.MoveTo(0, 0)
- p.LineTo(width, 0)
- p.LineTo(width, height)
- p.LineTo(0, height)
- p.LineTo(0, 0)
- } else {
- // setting the background color does not use boxes
- r.MoveTo(0, 0)
- r.LineTo(width, 0)
- r.LineTo(width, height)
- r.LineTo(0, height)
- r.LineTo(0, 0)
- }
- p.FillStroke()
- return p
+// SetBackground fills the entire painter area with the given color.
+func (p *Painter) SetBackground(width, height int, color Color) {
+ p.moveTo(0, 0)
+ p.lineTo(width, 0)
+ p.lineTo(width, height)
+ p.lineTo(0, height)
+ p.lineTo(0, 0)
+ p.fill(color)
}
// MarkLine draws a horizontal line with a small circle and arrow at the right.
-func (p *Painter) MarkLine(x, y, width int) *Painter {
+func (p *Painter) MarkLine(x, y, width int, fillColor, strokeColor Color, strokeWidth float64, strokeDashArray []float64) {
arrowWidth := 16
arrowHeight := 10
endX := x + width
radius := 3
- p.Circle(3, x+radius, y)
- p.render.Fill()
- p.MoveTo(x+radius*3, y)
- p.LineTo(endX-arrowWidth, y)
- p.Stroke()
- p.ArrowRight(endX, y, arrowWidth, arrowHeight)
- return p
+
+ // Set up stroke style before drawing
+ defer p.render.ResetStyle()
+ p.render.SetStrokeColor(strokeColor)
+ p.render.SetStrokeWidth(strokeWidth)
+ p.render.SetStrokeDashArray(strokeDashArray)
+ p.render.SetFillColor(fillColor)
+
+ // Draw the circle at the starting point
+ p.render.Circle(float64(radius), x+radius+p.box.Left, y+p.box.Top)
+ p.render.Fill() // only fill the circle, do not stroke
+
+ // Draw the line from the end of the circle to near the arrow start
+ p.moveTo(x+radius*3, y)
+ p.lineTo(endX-arrowWidth, y)
+ p.render.Stroke() // apply stroke with the dash array
+
+ p.ArrowRight(endX, y, arrowWidth, arrowHeight, fillColor, strokeColor, strokeWidth)
}
// Polygon draws a polygon with the specified center, radius, and number of sides.
-func (p *Painter) Polygon(center Point, radius float64, sides int) *Painter {
+func (p *Painter) Polygon(center Point, radius float64, sides int, strokeColor Color, strokeWidth float64) {
points := getPolygonPoints(center, radius, sides)
- for i, item := range points {
- if i == 0 {
- p.MoveTo(item.X, item.Y)
- } else {
- p.LineTo(item.X, item.Y)
+ p.drawStraightPath(points, false)
+ p.lineTo(points[0].X, points[0].Y)
+ p.stroke(strokeColor, strokeWidth)
+}
+
+// Pin draws a pin shape (circle + curved tail).
+func (p *Painter) Pin(x, y, width int, fillColor, strokeColor Color, strokeWidth float64) {
+ r := float64(width) / 2
+ y -= width / 4
+ angle := chartdraw.DegreesToRadians(15)
+
+ // Draw the pin head with fill and stroke
+ startAngle := math.Pi/2 + angle
+ delta := 2*math.Pi - 2*angle
+ p.arcTo(x, y, r, r, startAngle, delta)
+ p.lineTo(x, y)
+ p.close()
+ p.fillStroke(fillColor, strokeColor, strokeWidth)
+
+ // The curved tail
+ startX := x - int(r)
+ startY := y
+ endX := x + int(r)
+ endY := y
+ p.moveTo(startX, startY)
+ cx := x
+ cy := y + int(r*2.5)
+ p.quadCurveTo(cx, cy, endX, endY)
+ p.close()
+
+ // Apply both fill and stroke to the tail
+ p.fillStroke(fillColor, strokeColor, strokeWidth)
+}
+
+// arrow draws an arrow shape in the given direction, then fill+stroke with the given style.
+func (p *Painter) arrow(x, y, width, height int, direction string,
+ fillColor, strokeColor Color, strokeWidth float64) {
+ halfWidth := width >> 1
+ halfHeight := height >> 1
+ if direction == PositionTop || direction == PositionBottom {
+ x0 := x - halfWidth
+ x1 := x0 + width
+ dy := -height / 3
+ y0 := y
+ y1 := y0 - height
+ if direction == PositionBottom {
+ y0 = y - height
+ y1 = y
+ dy = 2 * dy
}
+ p.moveTo(x0, y0)
+ p.lineTo(x0+halfWidth, y1)
+ p.lineTo(x1, y0)
+ p.lineTo(x0+halfWidth, y+dy)
+ p.lineTo(x0, y0)
+ } else {
+ x0 := x + width
+ x1 := x0 - width
+ y0 := y - halfHeight
+ dx := -width / 3
+ if direction == PositionRight {
+ x0 = x - width
+ dx = -dx
+ x1 = x0 + width
+ }
+ p.moveTo(x0, y0)
+ p.lineTo(x1, y0+halfHeight)
+ p.lineTo(x0, y0+height)
+ p.lineTo(x0+dx, y0+halfHeight)
+ p.lineTo(x0, y0)
}
- p.LineTo(points[0].X, points[0].Y)
- p.Stroke()
- return p
+ p.fillStroke(fillColor, strokeColor, strokeWidth)
+}
+
+// ArrowLeft draws an arrow at the given point and dimensions pointing left.
+func (p *Painter) ArrowLeft(x, y, width, height int,
+ fillColor, strokeColor Color, strokeWidth float64) {
+ p.arrow(x, y, width, height, PositionLeft, fillColor, strokeColor, strokeWidth)
+}
+
+// ArrowRight draws an arrow at the given point and dimensions pointing right.
+func (p *Painter) ArrowRight(x, y, width, height int,
+ fillColor, strokeColor Color, strokeWidth float64) {
+ p.arrow(x, y, width, height, PositionRight, fillColor, strokeColor, strokeWidth)
}
-// FillArea draws a filled polygon through the given points, skipping "null" (MaxInt32) break values (filling the area
-// flat between them).
-func (p *Painter) FillArea(points []Point) *Painter {
+// ArrowUp draws an arrow at the given point and dimensions pointing up.
+func (p *Painter) ArrowUp(x, y, width, height int,
+ fillColor, strokeColor Color, strokeWidth float64) {
+ p.arrow(x, y, width, height, PositionTop, fillColor, strokeColor, strokeWidth)
+}
+
+// ArrowDown draws an arrow at the given point and dimensions pointing down.
+func (p *Painter) ArrowDown(x, y, width, height int,
+ fillColor, strokeColor Color, strokeWidth float64) {
+ p.arrow(x, y, width, height, PositionBottom, fillColor, strokeColor, strokeWidth)
+}
+
+// FillArea draws a filled polygon through the given points, skipping "null" (MaxInt32) break values
+// (filling the area flat between them).
+func (p *Painter) FillArea(points []Point, fillColor Color) {
if len(points) == 0 {
- return p
+ return
}
var valid []Point
@@ -629,7 +520,7 @@ func (p *Painter) FillArea(points []Point) *Painter {
if pt.Y == math.MaxInt32 {
// If we encounter a break, fill the accumulated segment
p.drawStraightPath(valid, false)
- p.Fill()
+ p.fill(fillColor)
valid = valid[:0] // reset
continue
}
@@ -638,16 +529,15 @@ func (p *Painter) FillArea(points []Point) *Painter {
// Fill the last segment if there is one
p.drawStraightPath(valid, false)
- p.Fill()
-
- return p
+ p.fill(fillColor)
}
-// smoothFillArea draws a smooth curve for the "top" portion of points but uses straight lines for the bottom corners,
-// producing a fill with sharp corners.
-func (p *Painter) smoothFillChartArea(points []Point, tension float64) *Painter {
+// smoothFillChartArea draws a smooth curve for the "top" portion of points but uses straight lines for
+// the bottom corners, producing a fill with sharp corners.
+func (p *Painter) smoothFillChartArea(points []Point, tension float64, fillColor Color) {
if tension <= 0 {
- return p.FillArea(points)
+ p.FillArea(points, fillColor)
+ return
} else if tension > 1 {
tension = 1
}
@@ -657,16 +547,18 @@ func (p *Painter) smoothFillChartArea(points []Point, tension float64) *Painter
// We'll separate them:
if len(points) < 3 {
// Not enough to separate top from bottom
- return p.FillArea(points)
+ p.FillArea(points, fillColor)
+ return
}
// The final 3 points are the corners + repeated first point
top := points[:len(points)-3]
- bottom := points[len(points)-3:] // [ corner1, corner2, firstTopAgain ]
+ bottom := points[len(points)-3:] // [corner1, corner2, firstTopAgain]
// If top portion is empty or 1 point, just fill straight
if len(top) < 2 {
- return p.FillArea(points)
+ p.FillArea(points, fillColor)
+ return
}
// Build the smooth path for the top portion
@@ -692,50 +584,59 @@ func (p *Painter) smoothFillChartArea(points []Point, tension float64) *Painter
}
if !firstPointSet {
- return p.FillArea(points) // No actual top segment was drawn, fallback to straight fill
+ p.FillArea(points, fillColor) // No actual top segment was drawn, fallback to straight fill
+ return
}
// Add sharp lines to close the shape at the bottom
// The path is currently at the last top point we drew. Now we need to draw to corner1 -> corner2 -> firstTopAgain
for i := 0; i < len(bottom); i++ {
- p.LineTo(bottom[i].X, bottom[i].Y)
+ p.lineTo(bottom[i].X, bottom[i].Y)
}
-
- p.Fill()
- return p
+ p.fill(fillColor)
}
-func (p *Painter) Text(body string, x, y int) *Painter {
- p.render.Text(body, x+p.box.Left, y+p.box.Top)
- return p
-}
+// Text draws the given string at the position specified, using the given font style. Specifying radians will rotate
+// the text.
+func (p *Painter) Text(body string, x, y int, radians float64, fontStyle FontStyle) {
+ if fontStyle.Font == nil {
+ fontStyle.Font = getPreferredFont(p.font)
+ }
+ defer p.render.ResetStyle()
+ p.render.SetFont(fontStyle.Font)
+ p.render.SetFontSize(fontStyle.FontSize)
+ p.render.SetFontColor(fontStyle.FontColor)
-func (p *Painter) TextRotation(body string, x, y int, radians float64) {
- p.render.SetTextRotation(radians)
+ if radians != 0 {
+ defer p.render.ClearTextRotation()
+ p.render.SetTextRotation(radians)
+ }
p.render.Text(body, x+p.box.Left, y+p.box.Top)
- p.render.ClearTextRotation()
}
-func (p *Painter) SetTextRotation(radians float64) {
- p.render.SetTextRotation(radians)
-}
-func (p *Painter) ClearTextRotation() {
- p.render.ClearTextRotation()
-}
-
-func (p *Painter) TextFit(body string, x, y, width int, textAligns ...string) chartdraw.Box {
- style := p.style
- textWarp := style.TextWrap
- style.TextWrap = chartdraw.TextWrapWord
+// TextFit draws multi-line text constrained to a given width.
+func (p *Painter) TextFit(body string, x, y, width int, fontStyle FontStyle, textAligns ...string) chartdraw.Box {
+ if fontStyle.Font == nil {
+ fontStyle.Font = getPreferredFont(p.font)
+ }
+ style := chartdraw.Style{
+ FontStyle: fontStyle,
+ TextWrap: chartdraw.TextWrapWord,
+ }
r := p.render
+ defer r.ResetStyle()
+ r.SetFont(fontStyle.Font)
+ r.SetFontSize(fontStyle.FontSize)
+ r.SetFontColor(fontStyle.FontColor)
+
lines := chartdraw.Text.WrapFit(r, body, width, style)
- p.SetFontStyle(style.FontStyle)
- var output chartdraw.Box
+ var output chartdraw.Box
textAlign := ""
if len(textAligns) != 0 {
textAlign = textAligns[0]
}
+
for index, line := range lines {
if line == "" {
continue
@@ -749,17 +650,19 @@ func (p *Painter) TextFit(body string, x, y, width int, textAligns ...string) ch
case AlignCenter:
x0 += (width - lineBox.Width()) >> 1
}
- p.Text(line, x0, y0)
+
+ p.render.Text(line, x0+p.box.Left, y0+p.box.Top)
output.Right = chartdraw.MaxInt(lineBox.Right, output.Right)
output.Bottom += lineBox.Height()
if index < len(lines)-1 {
- output.Bottom += +style.GetTextLineSpacing()
+ output.Bottom += style.GetTextLineSpacing()
}
}
- p.style.TextWrap = textWarp
+ output.IsSet = true
return output
}
+// isTick determines whether the given index is a "tick" mark out of numTicks.
func isTick(totalRange int, numTicks int, index int) bool {
if numTicks >= totalRange {
return true
@@ -779,95 +682,95 @@ func isTick(totalRange int, numTicks int, index int) bool {
return actualTickIndex == index
}
-func (p *Painter) Ticks(opt TicksOption) *Painter {
- if opt.LabelCount <= 0 || opt.Length <= 0 {
- return p
+// ticks draws small lines to indicate tick marks, using a fixed stroke color/width.
+func (p *Painter) ticks(opt ticksOption) {
+ if opt.labelCount <= 0 || opt.length <= 0 {
+ return
}
var values []int
- if opt.Vertical {
- values = autoDivide(p.Height(), opt.TickSpaces)
+ if opt.vertical {
+ values = autoDivide(p.Height(), opt.tickSpaces)
} else {
- values = autoDivide(p.Width(), opt.TickSpaces)
+ values = autoDivide(p.Width(), opt.tickSpaces)
}
for index, value := range values {
- if index < opt.First {
+ if index < opt.firstIndex {
continue
- } else if !isTick(len(values)-opt.First, opt.TickCount, index-opt.First) {
+ } else if !isTick(len(values)-opt.firstIndex, opt.tickCount, index-opt.firstIndex) {
continue
}
- if opt.Vertical {
+ if opt.vertical {
p.LineStroke([]Point{
{X: 0, Y: value},
- {X: opt.Length, Y: value},
- })
+ {X: opt.length, Y: value},
+ }, opt.strokeColor, opt.strokeWidth)
} else {
p.LineStroke([]Point{
- {X: value, Y: opt.Length},
+ {X: value, Y: opt.length},
{X: value, Y: 0},
- })
+ }, opt.strokeColor, opt.strokeWidth)
}
}
- return p
}
-func (p *Painter) MultiText(opt MultiTextOption) *Painter {
- if len(opt.TextList) == 0 {
- return p
+// multiText prints multiple lines of text for axis labels.
+func (p *Painter) multiText(opt multiTextOption) {
+ if len(opt.textList) == 0 {
+ return
}
- count := len(opt.TextList)
+ count := len(opt.textList)
width := p.Width()
height := p.Height()
var positions []int
- if opt.Vertical {
- if opt.CenterLabels {
+ if opt.vertical {
+ if opt.centerLabels {
positions = autoDivide(height, count)
} else {
positions = autoDivide(height, count-1)
}
} else {
- if opt.CenterLabels {
+ if opt.centerLabels {
positions = autoDivide(width, count)
} else {
positions = autoDivide(width, count-1)
}
}
- isTextRotation := opt.TextRotation != 0
+ if opt.textRotation != 0 {
+ defer p.render.ClearTextRotation()
+ p.render.SetTextRotation(opt.textRotation)
+ }
positionCount := len(positions)
- skippedLabels := opt.LabelSkipCount // specify the skip count to ensure the top value is listed
+ skippedLabels := opt.labelSkipCount // specify the skip count to ensure the top value is listed
for index, start := range positions {
- if opt.CenterLabels && index == positionCount-1 {
+ if opt.centerLabels && index == positionCount-1 {
break // positions have one item more than we can map to text, this extra value is used to center against
- } else if index < opt.First {
+ } else if index < opt.firstIndex {
continue
- } else if !opt.Vertical &&
+ } else if !opt.vertical &&
index != count-1 && // one off case for last label due to values and label qty difference
- !isTick(positionCount-opt.First, opt.TickCount, index-opt.First) {
+ !isTick(positionCount-opt.firstIndex, opt.tickCount, index-opt.firstIndex) {
continue
} else if index != count-1 && // ensure the bottom value is always printed
- skippedLabels < opt.LabelSkipCount {
+ skippedLabels < opt.labelSkipCount {
skippedLabels++
continue
} else {
skippedLabels = 0
}
- if isTextRotation {
- p.ClearTextRotation()
- p.SetTextRotation(opt.TextRotation)
- }
- text := opt.TextList[index]
- box := p.MeasureText(text)
+ text := opt.textList[index]
+ box := p.MeasureText(text, opt.textRotation, opt.fontStyle)
x := 0
y := 0
- if opt.Vertical {
- if opt.CenterLabels {
+ if opt.vertical {
+ if opt.centerLabels {
start = (positions[index] + positions[index+1]) >> 1
} else {
start = positions[index]
}
y = start + box.Height()>>1
- switch opt.Align {
+ switch opt.align {
case AlignRight:
x = width - box.Width()
case AlignCenter:
@@ -876,11 +779,11 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter {
x = 0
}
} else {
- if opt.CenterLabels {
+ if opt.centerLabels {
// graphs with limited data samples generally look better with the samples directly below the label
// for that reason we will exactly center these graphs, but graphs with higher sample counts will
// attempt to space the labels better rather than line up directly to the graph points
- exactLabels := count == opt.LabelCount
+ exactLabels := count == opt.labelCount
if !exactLabels && index == 0 {
x = start - 1 // align to the actual start (left side of tick space)
} else if !exactLabels && index == count-1 {
@@ -897,77 +800,37 @@ func (p *Painter) MultiText(opt MultiTextOption) *Painter {
}
}
}
- x += opt.Offset.Left
- y += opt.Offset.Top
- p.Text(text, x, y)
- }
- if isTextRotation {
- p.ClearTextRotation()
+ x += opt.offset.Left
+ y += opt.offset.Top
+ p.Text(text, x, y, opt.textRotation, opt.fontStyle)
}
- return p
}
-func (p *Painter) Grid(opt GridOption) *Painter {
- width := p.Width()
- height := p.Height()
- drawLines := func(values []int, ignoreIndexList []int, isVertical bool) {
- for index, v := range values {
- if containsInt(ignoreIndexList, index) {
- continue
- }
- x0 := 0
- y0 := 0
- x1 := 0
- y1 := 0
- if isVertical {
- x0 = v
- x1 = v
- y1 = height
- } else {
- x1 = width
- y0 = v
- y1 = v
- }
- p.LineStroke([]Point{
- {X: x0, Y: y0},
- {X: x1, Y: y1},
- })
- }
- }
- columnCount := sumInt(opt.ColumnSpans)
- if columnCount == 0 {
- columnCount = opt.Columns
- }
- if columnCount > 0 {
- values := autoDivideSpans(width, columnCount, opt.ColumnSpans)
- drawLines(values, opt.IgnoreColumnLines, true)
- }
- if opt.Rows > 0 {
- values := autoDivide(height, opt.Rows)
- drawLines(values, opt.IgnoreRowLines, false)
- }
- return p
-}
-
-func (p *Painter) Dots(points []Point) *Painter {
+// Dots prints filled circles for the given points.
+func (p *Painter) Dots(points []Point, fillColor, strokeColor Color, strokeWidth float64, dotRadius float64) {
+ defer p.render.ResetStyle()
+ p.render.SetFillColor(fillColor)
+ p.render.SetStrokeColor(strokeColor)
+ p.render.SetStrokeWidth(strokeWidth)
for _, item := range points {
- p.Circle(2, item.X, item.Y)
+ p.render.Circle(dotRadius, item.X+p.box.Left, item.Y+p.box.Top)
}
- p.FillStroke()
- return p
+ p.render.FillStroke()
}
-func (p *Painter) Rect(box Box) *Painter {
- p.MoveTo(box.Left, box.Top)
- p.LineTo(box.Right, box.Top)
- p.LineTo(box.Right, box.Bottom)
- p.LineTo(box.Left, box.Bottom)
- p.LineTo(box.Left, box.Top)
- p.FillStroke()
- return p
+// filledRect will draw a filled box with the given coordinates.
+func (p *Painter) filledRect(box Box, fillColor, strokeColor Color, strokeWidth float64) {
+ p.moveTo(box.Left, box.Top)
+ p.lineTo(box.Right, box.Top)
+ p.lineTo(box.Right, box.Bottom)
+ p.lineTo(box.Left, box.Bottom)
+ p.lineTo(box.Left, box.Top)
+ p.fillStroke(fillColor, strokeColor, strokeWidth)
}
-func (p *Painter) RoundedRect(box Box, radius int, roundTop, roundBottom bool) *Painter {
+// roundedRect is similar to filledRect except the top and bottom will be rounded.
+func (p *Painter) roundedRect(box Box, radius int, roundTop, roundBottom bool,
+ fillColor, strokeColor Color, strokeWidth float64) {
r := (box.Right - box.Left) / 2
if radius > r {
radius = r
@@ -977,65 +840,105 @@ func (p *Painter) RoundedRect(box Box, radius int, roundTop, roundBottom bool) *
if roundTop {
// Start at the appropriate point depending on rounding at the top
- p.MoveTo(box.Left+radius, box.Top)
- p.LineTo(box.Right-radius, box.Top)
+ p.moveTo(box.Left+radius, box.Top)
+ p.lineTo(box.Right-radius, box.Top)
// right top
cx := box.Right - radius
cy := box.Top + radius
- p.ArcTo(cx, cy, rx, ry, -math.Pi/2, math.Pi/2)
+ p.arcTo(cx, cy, rx, ry, -math.Pi/2, math.Pi/2)
} else {
- p.MoveTo(box.Left, box.Top)
- p.LineTo(box.Right, box.Top)
+ p.moveTo(box.Left, box.Top)
+ p.lineTo(box.Right, box.Top)
}
if roundBottom {
- p.LineTo(box.Right, box.Bottom-radius)
+ p.lineTo(box.Right, box.Bottom-radius)
// right bottom
cx := box.Right - radius
cy := box.Bottom - radius
- p.ArcTo(cx, cy, rx, ry, 0, math.Pi/2)
+ p.arcTo(cx, cy, rx, ry, 0, math.Pi/2)
- p.LineTo(box.Left+radius, box.Bottom)
+ p.lineTo(box.Left+radius, box.Bottom)
// left bottom
cx = box.Left + radius
cy = box.Bottom - radius
- p.ArcTo(cx, cy, rx, ry, math.Pi/2, math.Pi/2)
+ p.arcTo(cx, cy, rx, ry, math.Pi/2, math.Pi/2)
} else {
- p.LineTo(box.Right, box.Bottom)
- p.LineTo(box.Left, box.Bottom)
+ p.lineTo(box.Right, box.Bottom)
+ p.lineTo(box.Left, box.Bottom)
}
if roundTop {
// left top
- p.LineTo(box.Left, box.Top+radius)
+ p.lineTo(box.Left, box.Top+radius)
cx := box.Left + radius
cy := box.Top + radius
- p.ArcTo(cx, cy, rx, ry, math.Pi, math.Pi/2)
+ p.arcTo(cx, cy, rx, ry, math.Pi, math.Pi/2)
} else {
- p.LineTo(box.Left, box.Top)
+ p.lineTo(box.Left, box.Top)
}
- p.Close()
- p.FillStroke()
- p.Fill()
- return p
+ p.close()
+ p.fillStroke(fillColor, strokeColor, strokeWidth)
}
-func (p *Painter) LegendLineDot(box Box) *Painter {
- width := box.Width()
- height := box.Height()
- strokeWidth := 3
- dotHeight := 5
-
- p.render.SetStrokeWidth(float64(strokeWidth))
- center := (height-strokeWidth)>>1 - 1
- p.MoveTo(box.Left, box.Top-center)
- p.LineTo(box.Right, box.Top-center)
- p.Stroke()
- p.Circle(float64(dotHeight), box.Left+width>>1, box.Top-center)
- p.FillStroke()
- return p
+// legendLineDot draws a small horizontal line with a dot in the middle, often used in legends.
+func (p *Painter) legendLineDot(box Box, strokeColor Color, strokeWidth float64, dotColor Color) {
+ center := (box.Height()-int(strokeWidth))>>1 - 1
+
+ defer p.render.ResetStyle()
+ p.render.SetStrokeColor(strokeColor)
+ p.render.SetStrokeWidth(strokeWidth)
+ p.moveTo(box.Left, box.Top-center)
+ p.lineTo(box.Right, box.Top-center)
+ p.render.Stroke()
+
+ // draw dot in the middle
+ midX := box.Left + (box.Width() >> 1)
+ p.Circle(5, midX, box.Top-center, dotColor, dotColor, 3)
+}
+
+// BarChart renders a bar chart with the provided configuration to the painter.
+func (p *Painter) BarChart(opt BarChartOption) error {
+ _, err := newBarChart(p, opt).Render()
+ return err
+}
+
+// HorizontalBarChart renders a horizontal bar chart with the provided configuration to the painter.
+func (p *Painter) HorizontalBarChart(opt HorizontalBarChartOption) error {
+ _, err := newHorizontalBarChart(p, opt).Render()
+ return err
+}
+
+// FunnelChart renders a funnel chart with the provided configuration to the painter.
+func (p *Painter) FunnelChart(opt FunnelChartOption) error {
+ _, err := newFunnelChart(p, opt).Render()
+ return err
+}
+
+// LineChart renders a line chart with the provided configuration to the painter.
+func (p *Painter) LineChart(opt LineChartOption) error {
+ _, err := newLineChart(p, opt).Render()
+ return err
+}
+
+// PieChart renders a pie chart with the provided configuration to the painter.
+func (p *Painter) PieChart(opt PieChartOption) error {
+ _, err := newPieChart(p, opt).Render()
+ return err
+}
+
+// RadarChart renders a radar chart with the provided configuration to the painter.
+func (p *Painter) RadarChart(opt RadarChartOption) error {
+ _, err := newRadarChart(p, opt).Render()
+ return err
+}
+
+// TableChart renders a table with the provided configuration to the painter.
+func (p *Painter) TableChart(opt TableChartOption) error {
+ _, err := newTableChart(p, opt).Render()
+ return err
}
diff --git a/painter_test.go b/painter_test.go
index 44f43a6..d435a19 100644
--- a/painter_test.go
+++ b/painter_test.go
@@ -1,11 +1,11 @@
package charts
import (
+ "fmt"
"math"
"strconv"
"testing"
- "github.com/golang/freetype/truetype"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -16,29 +16,49 @@ import (
func TestPainterOption(t *testing.T) {
t.Parallel()
- font := &truetype.Font{}
- d, err := NewPainter(PainterOptions{
- OutputFormat: ChartOutputSVG,
- Width: 800,
- Height: 600,
- },
- PainterBoxOption(Box{Right: 400, Bottom: 300}),
- PainterPaddingOption(Box{Left: 1, Top: 2, Right: 3, Bottom: 4}),
- PainterFontOption(font),
- PainterStyleOption(chartdraw.Style{ClassName: "test"}),
- )
- require.NoError(t, err)
- assert.Equal(t, Box{
- Left: 1,
- Top: 2,
- Right: 397,
- Bottom: 296,
- }, d.box)
- assert.Equal(t, font, d.font)
- assert.Equal(t, "test", d.style.ClassName)
+ t.Run("box", func(t *testing.T) {
+ p := NewPainter(PainterOptions{
+ OutputFormat: ChartOutputSVG,
+ Width: 800,
+ Height: 600,
+ },
+ PainterBoxOption(Box{Right: 400, Bottom: 300}),
+ PainterPaddingOption(Box{Left: 1, Top: 2, Right: 3, Bottom: 4}),
+ )
+
+ assert.Equal(t, Box{
+ Left: 1,
+ Top: 2,
+ Right: 397,
+ Bottom: 296,
+ }, p.box)
+ })
+ t.Run("theme", func(t *testing.T) {
+ theme := GetTheme(ThemeAnt)
+ p := NewPainter(PainterOptions{
+ OutputFormat: ChartOutputSVG,
+ Width: 800,
+ Height: 600,
+ }, PainterThemeOption(theme))
+
+ assert.Equal(t, theme, p.theme)
+ })
+ t.Run("font", func(t *testing.T) {
+ font := GetDefaultFont()
+ p := NewPainter(PainterOptions{
+ OutputFormat: ChartOutputSVG,
+ Width: 800,
+ Height: 600,
+ })
+ require.Nil(t, p.font)
+
+ p = p.Child(PainterFontOption(font))
+
+ assert.Equal(t, font, p.font)
+ })
}
-func TestPainter(t *testing.T) {
+func TestPainterInternal(t *testing.T) {
t.Parallel()
tests := []struct {
@@ -46,192 +66,202 @@ func TestPainter(t *testing.T) {
fn func(*Painter)
result string
}{
+ {
+ name: "circle",
+ fn: func(p *Painter) {
+ p.Circle(5, 2, 3, drawing.ColorTransparent, drawing.ColorTransparent, 1.0)
+ },
+ result: "",
+ },
{
name: "moveTo_lineTo",
fn: func(p *Painter) {
- p.MoveTo(1, 1)
- p.LineTo(2, 2)
- p.Stroke()
+ p.moveTo(1, 1)
+ p.lineTo(2, 2)
+ p.stroke(drawing.ColorTransparent, 1.0)
},
- result: "",
+ result: "",
},
{
- name: "circle",
+ name: "arc",
fn: func(p *Painter) {
- p.Circle(5, 2, 3)
+ p.arcTo(100, 100, 100, 100, 0, math.Pi/2)
+ p.close()
+ p.fillStroke(drawing.ColorBlue, drawing.ColorBlack, 1)
},
- result: "",
+ result: "",
},
+ }
+
+ for i, tt := range tests {
+ t.Run(strconv.Itoa(i)+"-"+tt.name, func(t *testing.T) {
+ p := NewPainter(PainterOptions{
+ OutputFormat: ChartOutputSVG,
+ Width: 400,
+ Height: 300,
+ }, PainterPaddingOption(chartdraw.Box{Left: 5, Top: 10}))
+ tt.fn(p)
+ data, err := p.Bytes()
+ require.NoError(t, err)
+ assertEqualSVG(t, tt.result, data)
+ })
+ }
+}
+
+func TestPainterExternal(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ fn func(*Painter)
+ result string
+ }{
{
name: "text",
fn: func(p *Painter) {
- p.Text("hello world!", 3, 6)
+ p.Text("hello world!", 3, 6, 0, FontStyle{})
+ },
+ result: "",
+ },
+ {
+ name: "text_rotated",
+ fn: func(p *Painter) {
+ p.Text("hello world!", 3, 6, chartdraw.DegreesToRadians(90), FontStyle{})
},
- result: "",
+ result: "",
},
{
- name: "line",
+ name: "line_stroke",
fn: func(p *Painter) {
- p.SetDrawingStyle(chartdraw.Style{
- StrokeColor: drawing.ColorBlack,
- StrokeWidth: 1,
- })
p.LineStroke([]Point{
- {X: 1, Y: 2},
- {X: 3, Y: 4},
- })
+ {X: 10, Y: 20},
+ {X: 30, Y: 40},
+ {X: 50, Y: 20},
+ }, drawing.ColorBlack, 1)
},
- result: "",
+ result: "",
},
{
- name: "background",
+ name: "smooth_line_stroke",
fn: func(p *Painter) {
- p.SetBackground(400, 300, chartdraw.ColorWhite)
+ p.SmoothLineStroke([]Point{
+ {X: 10, Y: 20},
+ {X: 20, Y: 40},
+ {X: 30, Y: 60},
+ {X: 40, Y: 50},
+ {X: 50, Y: 40},
+ {X: 60, Y: 80},
+ }, 0.5, drawing.ColorBlack, 1)
},
- result: "",
+ result: "",
},
{
- name: "arc",
+ name: "background",
fn: func(p *Painter) {
- p.SetStyle(chartdraw.Style{
- StrokeWidth: 1,
- StrokeColor: drawing.ColorBlack,
- FillColor: drawing.ColorBlue,
- })
- p.ArcTo(100, 100, 100, 100, 0, math.Pi/2)
- p.Close()
- p.FillStroke()
+ p.SetBackground(400, 300, chartdraw.ColorWhite)
},
- result: "",
+ result: "",
},
{
name: "pin",
fn: func(p *Painter) {
- p.SetStyle(chartdraw.Style{
- StrokeWidth: 1,
- StrokeColor: Color{R: 84, G: 112, B: 198, A: 255},
- FillColor: Color{R: 84, G: 112, B: 198, A: 255},
- })
- p.Pin(30, 30, 30)
+ c := Color{R: 84, G: 112, B: 198, A: 255}
+ p.Pin(30, 30, 30, c, c, 1)
},
- result: "",
+ result: "",
},
{
name: "arrow_left",
fn: func(p *Painter) {
- p.SetStyle(chartdraw.Style{
- StrokeWidth: 1,
- StrokeColor: Color{R: 84, G: 112, B: 198, A: 255},
- FillColor: Color{R: 84, G: 112, B: 198, A: 255},
- })
- p.ArrowLeft(30, 30, 16, 10)
+ c := Color{R: 84, G: 112, B: 198, A: 255}
+ p.ArrowLeft(30, 30, 16, 10, c, c, 1)
},
- result: "",
+ result: "",
},
{
name: "arrow_right",
fn: func(p *Painter) {
- p.SetStyle(chartdraw.Style{
- StrokeWidth: 1,
- StrokeColor: Color{R: 84, G: 112, B: 198, A: 255},
- FillColor: Color{R: 84, G: 112, B: 198, A: 255},
- })
- p.ArrowRight(30, 30, 16, 10)
+ c := Color{R: 84, G: 112, B: 198, A: 255}
+ p.ArrowRight(30, 30, 16, 10, c, c, 1)
},
- result: "",
+ result: "",
},
{
- name: "arrow_top",
+ name: "arrow_up",
fn: func(p *Painter) {
- p.SetStyle(chartdraw.Style{
- StrokeWidth: 1,
- StrokeColor: Color{R: 84, G: 112, B: 198, A: 255},
- FillColor: Color{R: 84, G: 112, B: 198, A: 255},
- })
- p.ArrowTop(30, 30, 10, 16)
+ c := Color{R: 84, G: 112, B: 198, A: 255}
+ p.ArrowUp(30, 30, 10, 16, c, c, 1)
},
- result: "",
+ result: "",
},
{
- name: "arrow_bottom",
+ name: "arrow_down",
fn: func(p *Painter) {
- p.SetStyle(chartdraw.Style{
- StrokeWidth: 1,
- StrokeColor: Color{R: 84, G: 112, B: 198, A: 255},
- FillColor: Color{R: 84, G: 112, B: 198, A: 255},
- })
- p.ArrowBottom(30, 30, 10, 16)
+ c := Color{R: 84, G: 112, B: 198, A: 255}
+ p.ArrowDown(30, 30, 10, 16, c, c, 1)
},
- result: "",
+ result: "",
},
{
name: "mark_line",
fn: func(p *Painter) {
- p.SetStyle(chartdraw.Style{
- StrokeWidth: 1,
- StrokeColor: Color{R: 84, G: 112, B: 198, A: 255},
- FillColor: Color{R: 84, G: 112, B: 198, A: 255},
- StrokeDashArray: []float64{4, 2},
- })
- p.MarkLine(0, 20, 300)
+ c := Color{R: 84, G: 112, B: 198, A: 255}
+ p.MarkLine(0, 20, 300, c, c, 1, []float64{4, 2})
},
- result: "",
+ result: "",
},
{
name: "polygon",
fn: func(p *Painter) {
- p.SetStyle(chartdraw.Style{
- StrokeWidth: 1,
- StrokeColor: Color{R: 84, G: 112, B: 198, A: 255},
- })
- p.Polygon(Point{X: 100, Y: 100}, 50, 6)
+ p.Polygon(Point{X: 100, Y: 100}, 50, 6, Color{R: 84, G: 112, B: 198, A: 255}, 1)
},
- result: "",
+ result: "",
},
{
name: "fill_area",
fn: func(p *Painter) {
- p.SetDrawingStyle(chartdraw.Style{
- FillColor: Color{R: 84, G: 112, B: 198, A: 255},
- })
p.FillArea([]Point{
{X: 0, Y: 0},
{X: 0, Y: 100},
{X: 100, Y: 100},
{X: 0, Y: 0},
- })
+ }, Color{R: 84, G: 112, B: 198, A: 255})
},
- result: "",
+ result: "",
},
{
name: "child_chart",
fn: func(p *Painter) {
- _, _ = NewLineChart(p, makeMinimalLineChartOption()).Render()
- p = p.Child(PainterBoxOption(chartdraw.NewBox(0, 200, 400, 200)))
opt := makeMinimalLineChartOption()
+ opt.ValueFormatter = func(f float64) string {
+ return fmt.Sprintf("%.0f", f)
+ }
+ _ = p.LineChart(opt)
+ p = p.Child(PainterBoxOption(chartdraw.NewBox(0, 200, 400, 200)))
+ opt = makeMinimalLineChartOption()
opt.Theme = GetDefaultTheme().WithBackgroundColor(drawing.ColorFromAlphaMixedRGBA(0, 0, 0, 0))
- _, _ = NewLineChart(p, opt).Render()
+ _ = p.LineChart(opt)
},
- result: "",
+ result: "",
},
}
+
for i, tt := range tests {
t.Run(strconv.Itoa(i)+"-"+tt.name, func(t *testing.T) {
- d, err := NewPainter(PainterOptions{
+ p := NewPainter(PainterOptions{
OutputFormat: ChartOutputSVG,
Width: 400,
Height: 300,
}, PainterPaddingOption(chartdraw.Box{Left: 5, Top: 10}))
- require.NoError(t, err)
- tt.fn(d)
- data, err := d.Bytes()
+ tt.fn(p)
+ data, err := p.Bytes()
require.NoError(t, err)
assertEqualSVG(t, tt.result, data)
})
}
}
-func TestRoundedRect(t *testing.T) {
+func TestPainterRoundedRect(t *testing.T) {
t.Parallel()
tests := []struct {
@@ -242,54 +272,48 @@ func TestRoundedRect(t *testing.T) {
{
name: "round_fully",
fn: func(p *Painter) {
- p.RoundedRect(Box{
+ p.roundedRect(Box{
Left: 10,
Right: 30,
Bottom: 150,
Top: 10,
- }, 5, true, true)
+ }, 5, true, true, drawing.ColorBlue, drawing.ColorBlue, 1)
},
- result: "",
+ result: "",
},
{
name: "square_top",
fn: func(p *Painter) {
- p.RoundedRect(Box{
+ p.roundedRect(Box{
Left: 10,
Right: 30,
Bottom: 150,
Top: 10,
- }, 5, false, true)
+ }, 5, false, true, drawing.ColorBlue, drawing.ColorBlue, 1)
},
- result: "",
+ result: "",
},
{
name: "square_bottom",
fn: func(p *Painter) {
- p.RoundedRect(Box{
+ p.roundedRect(Box{
Left: 10,
Right: 30,
Bottom: 150,
Top: 10,
- }, 5, true, false)
+ }, 5, true, false, drawing.ColorBlue, drawing.ColorBlue, 1)
},
- result: "",
+ result: "",
},
}
for i, tc := range tests {
t.Run(strconv.Itoa(i)+"-"+tc.name, func(t *testing.T) {
- p, err := NewPainter(PainterOptions{
+ p := NewPainter(PainterOptions{
Width: 400,
Height: 300,
OutputFormat: ChartOutputSVG,
})
- require.NoError(t, err)
- p.OverrideDrawingStyle(chartdraw.Style{
- FillColor: drawing.ColorBlue,
- StrokeWidth: 1,
- StrokeColor: drawing.ColorBlue,
- })
tc.fn(p)
buf, err := p.Bytes()
require.NoError(t, err)
@@ -298,41 +322,64 @@ func TestRoundedRect(t *testing.T) {
}
}
-func TestPainterTextFit(t *testing.T) {
+func TestPainterMeasureText(t *testing.T) {
t.Parallel()
- p, err := NewPainter(PainterOptions{
+ svgP := NewPainter(PainterOptions{
OutputFormat: ChartOutputSVG,
Width: 400,
Height: 300,
})
- require.NoError(t, err)
+ pngP := NewPainter(PainterOptions{
+ OutputFormat: ChartOutputPNG,
+ Width: 400,
+ Height: 300,
+ })
style := FontStyle{
FontSize: 12,
FontColor: chartdraw.ColorBlack,
Font: GetDefaultFont(),
}
- p.SetStyle(chartdraw.Style{FontStyle: style})
- box := p.TextFit("Hello World!", 0, 20, 80)
- assert.Equal(t, chartdraw.Box{Right: 45, Bottom: 35}, box)
- box = p.TextFit("Hello World!", 0, 100, 200)
- assert.Equal(t, chartdraw.Box{Right: 84, Bottom: 15}, box)
+ assert.Equal(t, chartdraw.Box{Right: 84, Bottom: 15, IsSet: true},
+ svgP.MeasureText("Hello World!", 0, style))
+ assert.Equal(t, chartdraw.Box{Right: 99, Bottom: 14, IsSet: true},
+ pngP.MeasureText("Hello World!", 0, style))
+}
+
+func TestPainterTextFit(t *testing.T) {
+ t.Parallel()
+
+ p := NewPainter(PainterOptions{
+ OutputFormat: ChartOutputSVG,
+ Width: 400,
+ Height: 300,
+ })
+ fontStyle := FontStyle{
+ FontSize: 12,
+ FontColor: chartdraw.ColorBlack,
+ Font: GetDefaultFont(),
+ }
+
+ box := p.TextFit("Hello World!", 0, 20, 80, fontStyle)
+ assert.Equal(t, chartdraw.Box{Right: 45, Bottom: 35, IsSet: true}, box)
+
+ box = p.TextFit("Hello World!", 0, 100, 200, fontStyle)
+ assert.Equal(t, chartdraw.Box{Right: 84, Bottom: 15, IsSet: true}, box)
buf, err := p.Bytes()
require.NoError(t, err)
- assertEqualSVG(t, "", buf)
+ assertEqualSVG(t, "", buf)
}
func TestMultipleChartsOnPainter(t *testing.T) {
t.Parallel()
- p, err := NewPainter(PainterOptions{
+ p := NewPainter(PainterOptions{
OutputFormat: ChartOutputSVG,
Width: 800,
Height: 600,
})
- require.NoError(t, err)
p.SetBackground(800, 600, drawing.ColorWhite)
// set the space and theme for each chart
topCenterPainter := p.Child(PainterBoxOption(chartdraw.NewBox(0, 0, 800, 300)),
@@ -344,14 +391,14 @@ func TestMultipleChartsOnPainter(t *testing.T) {
pieOpt := makeBasicPieChartOption()
pieOpt.Legend.Show = False()
- _, err = NewPieChart(bottomLeftPainter, pieOpt).Render()
+ err := bottomLeftPainter.PieChart(pieOpt)
require.NoError(t, err)
- _, err = NewBarChart(bottomRightPainter, makeBasicBarChartOption()).Render()
+ err = bottomRightPainter.BarChart(makeBasicBarChartOption())
require.NoError(t, err)
- _, err = NewLineChart(topCenterPainter, makeBasicLineChartOption()).Render()
+ err = topCenterPainter.LineChart(makeBasicLineChartOption())
require.NoError(t, err)
buf, err := p.Bytes()
require.NoError(t, err)
- assertEqualSVG(t, "", buf)
+ assertEqualSVG(t, "", buf)
}
diff --git a/pie_chart.go b/pie_chart.go
index 96a9f94..bb48072 100644
--- a/pie_chart.go
+++ b/pie_chart.go
@@ -14,6 +14,16 @@ type pieChart struct {
opt *PieChartOption
}
+// NewPieChartOptionWithData returns an initialized PieChartOption with the SeriesList set for the provided data slice.
+func NewPieChartOptionWithData(data []float64) PieChartOption {
+ return PieChartOption{
+ SeriesList: NewSeriesListPie(data),
+ Padding: defaultPadding,
+ Theme: GetDefaultTheme(),
+ Font: GetDefaultFont(),
+ }
+}
+
type PieChartOption struct {
// Theme specifies the colors used for the pie chart.
Theme ColorPalette
@@ -31,8 +41,8 @@ type PieChartOption struct {
backgroundIsFilled bool
}
-// NewPieChart returns a pie chart renderer
-func NewPieChart(p *Painter, opt PieChartOption) *pieChart {
+// newPieChart returns a pie chart renderer.
+func newPieChart(p *Painter, opt PieChartOption) *pieChart {
return &pieChart{
p: p,
opt: &opt,
@@ -56,13 +66,12 @@ type sector struct {
lineBranchY int
lineEndX int
lineEndY int
- showLabel bool
label string
series Series
color Color
}
-func NewSector(cx int, cy int, radius float64, labelRadius float64, value float64, currentValue float64, totalValue float64, labelLineLength int, label string, series Series, color Color) sector {
+func newSector(cx int, cy int, radius float64, labelRadius float64, value float64, currentValue float64, totalValue float64, labelLineLength int, label string, series Series, color Color) sector {
s := sector{}
s.value = value
s.percent = value / totalValue
@@ -95,8 +104,9 @@ func NewSector(cx int, cy int, radius float64, labelRadius float64, value float6
s.lineEndY = s.lineBranchY
s.series = series
s.color = color
- s.showLabel = series.Label.Show
- s.label = NewPieLabelFormatter([]string{label}, series.Label.Formatter)(0, s.value, s.percent)
+ if !flagIs(false, series.Label.Show) { // only set the label if it's being rendered
+ s.label = labelFormatPie([]string{label}, series.Label.Formatter, 0, s.value, s.percent)
+ }
return s
}
@@ -138,10 +148,7 @@ func (p *pieChart) render(result *defaultRenderResult, seriesList SeriesList) (B
if len(series.Radius) != 0 {
radiusValue = series.Radius
}
- value := float64(0)
- for _, item := range series.Data {
- value += item.Value
- }
+ value := chartdraw.SumFloat64(series.Data...)
values[index] = value
total += value
}
@@ -177,7 +184,7 @@ func (p *pieChart) render(result *defaultRenderResult, seriesList SeriesList) (B
color = theme.GetSeriesColor(1)
}
}
- s := NewSector(cx, cy, radius, labelRadius, v, currentValue, total, labelLineWidth, seriesNames[index], series, color)
+ s := newSector(cx, cy, radius, labelRadius, v, currentValue, total, labelLineWidth, seriesNames[index], series, color)
switch quadrant := s.quadrant; quadrant {
case 1:
quadrant1 = append([]sector{s}, quadrant1...)
@@ -199,14 +206,12 @@ func (p *pieChart) render(result *defaultRenderResult, seriesList SeriesList) (B
maxY := 0
minY := 0
for _, s := range sectors {
- seriesPainter.OverrideDrawingStyle(chartdraw.Style{
- StrokeWidth: 1,
- StrokeColor: s.color,
- FillColor: s.color,
- })
- seriesPainter.MoveTo(s.cx, s.cy)
- seriesPainter.ArcTo(s.cx, s.cy, s.rx, s.ry, s.start, s.delta).LineTo(s.cx, s.cy).Close().FillStroke()
- if !s.showLabel {
+ seriesPainter.moveTo(s.cx, s.cy)
+ seriesPainter.arcTo(s.cx, s.cy, s.rx, s.ry, s.start, s.delta)
+ seriesPainter.lineTo(s.cx, s.cy)
+ seriesPainter.close()
+ seriesPainter.fillStroke(s.color, s.color, 1)
+ if s.label == "" {
continue
}
if currentQuadrant != s.quadrant {
@@ -235,11 +240,11 @@ func (p *pieChart) render(result *defaultRenderResult, seriesList SeriesList) (B
if prevY < minY {
minY = prevY
}
- seriesPainter.MoveTo(s.lineStartX, s.lineStartY)
- seriesPainter.LineTo(s.lineBranchX, s.lineBranchY)
- seriesPainter.MoveTo(s.lineBranchX, s.lineBranchY)
- seriesPainter.LineTo(s.lineEndX, s.lineEndY)
- seriesPainter.Stroke()
+ seriesPainter.moveTo(s.lineStartX, s.lineStartY)
+ seriesPainter.lineTo(s.lineBranchX, s.lineBranchY)
+ seriesPainter.moveTo(s.lineBranchX, s.lineBranchY)
+ seriesPainter.lineTo(s.lineEndX, s.lineEndY)
+ seriesPainter.stroke(s.color, 1)
textStyle := FontStyle{
FontColor: theme.GetTextColor(),
FontSize: labelFontSize,
@@ -248,9 +253,8 @@ func (p *pieChart) render(result *defaultRenderResult, seriesList SeriesList) (B
if !s.series.Label.FontStyle.FontColor.IsZero() {
textStyle.FontColor = s.series.Label.FontStyle.FontColor
}
- seriesPainter.OverrideFontStyle(textStyle)
- x, y := s.calculateTextXY(seriesPainter.MeasureText(s.label))
- seriesPainter.Text(s.label, x, y)
+ x, y := s.calculateTextXY(seriesPainter.MeasureText(s.label, 0, textStyle))
+ seriesPainter.Text(s.label, x, y, 0, textStyle)
}
return p.p.box, nil
}
@@ -262,19 +266,19 @@ func (p *pieChart) Render() (Box, error) {
}
renderResult, err := defaultRender(p.p, defaultRenderOption{
- Theme: opt.Theme,
- Padding: opt.Padding,
- SeriesList: opt.SeriesList,
- XAxis: XAxisOption{
+ theme: opt.Theme,
+ padding: opt.Padding,
+ seriesList: opt.SeriesList,
+ xAxis: &XAxisOption{
Show: False(),
},
- YAxis: []YAxisOption{
+ yAxis: []YAxisOption{
{
Show: False(),
},
},
- Title: opt.Title,
- Legend: opt.Legend,
+ title: opt.Title,
+ legend: &p.opt.Legend,
backgroundIsFilled: opt.backgroundIsFilled,
})
if err != nil {
diff --git a/pie_chart_test.go b/pie_chart_test.go
index 8ff602f..e5e00b2 100644
--- a/pie_chart_test.go
+++ b/pie_chart_test.go
@@ -4,6 +4,7 @@ import (
"strconv"
"testing"
+ "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/go-analyze/charts/chartdraw/drawing"
@@ -14,9 +15,7 @@ func makeBasicPieChartOption() PieChartOption {
1048, 735, 580, 484, 300,
}
return PieChartOption{
- SeriesList: NewPieSeriesList(values, PieSeriesOption{
- Label: SeriesLabel{Show: true},
- }),
+ SeriesList: NewSeriesListPie(values),
Title: TitleOption{
Text: "Rainfall vs Evaporation",
Subtext: "Fake Data",
@@ -29,7 +28,7 @@ func makeBasicPieChartOption() PieChartOption {
Left: 20,
},
Legend: LegendOption{
- Vertical: true,
+ Vertical: True(),
Data: []string{
"Search Engine", "Direct", "Email", "Union Ads", "Video Ads",
},
@@ -38,6 +37,19 @@ func makeBasicPieChartOption() PieChartOption {
}
}
+func TestNewPieChartOptionWithData(t *testing.T) {
+ t.Parallel()
+
+ opt := NewPieChartOptionWithData([]float64{12, 24, 48})
+
+ assert.Len(t, opt.SeriesList, 3)
+ assert.Equal(t, ChartTypePie, opt.SeriesList[0].Type)
+ assert.Equal(t, defaultPadding, opt.Padding)
+
+ p := NewPainter(PainterOptions{})
+ assert.NoError(t, p.PieChart(opt))
+}
+
func TestPieChart(t *testing.T) {
t.Parallel()
@@ -53,13 +65,13 @@ func TestPieChart(t *testing.T) {
name: "default",
defaultTheme: true,
makeOptions: makeBasicPieChartOption,
- result: "",
+ result: "",
},
{
name: "themed",
defaultTheme: false,
makeOptions: makeBasicPieChartOption,
- result: "",
+ result: "",
},
{
name: "lots_labels-sortedDescending",
@@ -75,9 +87,9 @@ func TestPieChart(t *testing.T) {
}
return PieChartOption{
- SeriesList: NewPieSeriesList(values, PieSeriesOption{
+ SeriesList: NewSeriesListPie(values, PieSeriesOption{
Label: SeriesLabel{
- Show: true,
+ Show: True(),
Formatter: "{b} ({c} ≅ {d})",
},
Radius: "200",
@@ -122,7 +134,7 @@ func TestPieChart(t *testing.T) {
},
}
},
- result: "",
+ result: "",
},
{
name: "lots_labels-unsorted",
@@ -138,9 +150,9 @@ func TestPieChart(t *testing.T) {
}
return PieChartOption{
- SeriesList: NewPieSeriesList(values, PieSeriesOption{
+ SeriesList: NewSeriesListPie(values, PieSeriesOption{
Label: SeriesLabel{
- Show: true,
+ Show: True(),
Formatter: "{b} ({c} ≅ {d})",
},
Radius: "200",
@@ -185,7 +197,7 @@ func TestPieChart(t *testing.T) {
},
}
},
- result: "",
+ result: "",
},
{
name: "100labels-sorted",
@@ -201,10 +213,7 @@ func TestPieChart(t *testing.T) {
}
return PieChartOption{
- SeriesList: NewPieSeriesList(values, PieSeriesOption{
- Label: SeriesLabel{
- Show: true,
- },
+ SeriesList: NewSeriesListPie(values, PieSeriesOption{
Radius: "200",
}),
Padding: Box{
@@ -219,7 +228,7 @@ func TestPieChart(t *testing.T) {
},
}
},
- result: "",
+ result: "",
},
{
name: "fix_label_pos",
@@ -235,9 +244,9 @@ func TestPieChart(t *testing.T) {
25804, 25730, 24438, 23782, 22896, 21404, 428978,
}
return PieChartOption{
- SeriesList: NewPieSeriesList(values, PieSeriesOption{
+ SeriesList: NewSeriesListPie(values, PieSeriesOption{
Label: SeriesLabel{
- Show: true,
+ Show: True(),
Formatter: "{b} ({c} ≅ {d})",
},
Radius: "150",
@@ -264,7 +273,7 @@ func TestPieChart(t *testing.T) {
},
}
},
- result: "",
+ result: "",
},
{
name: "custom_fonts",
@@ -279,7 +288,7 @@ func TestPieChart(t *testing.T) {
opt.Title.FontStyle = customFont
return opt
},
- result: "",
+ result: "",
},
{
name: "legend_bottom_right",
@@ -292,7 +301,7 @@ func TestPieChart(t *testing.T) {
}
return opt
},
- result: "",
+ result: "",
},
}
@@ -310,8 +319,7 @@ func TestPieChart(t *testing.T) {
}
if tt.defaultTheme {
t.Run(strconv.Itoa(i)+"-"+tt.name, func(t *testing.T) {
- p, err := NewPainter(painterOptions)
- require.NoError(t, err)
+ p := NewPainter(painterOptions)
validatePieChartRender(t, p.Child(PainterPaddingOption(Box{
Left: 20,
@@ -322,14 +330,12 @@ func TestPieChart(t *testing.T) {
})
} else {
t.Run(strconv.Itoa(i)+"-"+tt.name+"-painter", func(t *testing.T) {
- p, err := NewPainter(painterOptions, PainterThemeOption(GetTheme(ThemeVividDark)))
- require.NoError(t, err)
+ p := NewPainter(painterOptions, PainterThemeOption(GetTheme(ThemeVividDark)))
validatePieChartRender(t, p, tt.makeOptions(), tt.result)
})
t.Run(strconv.Itoa(i)+"-"+tt.name+"-options", func(t *testing.T) {
- p, err := NewPainter(painterOptions)
- require.NoError(t, err)
+ p := NewPainter(painterOptions)
opt := tt.makeOptions()
opt.Theme = GetTheme(ThemeVividDark)
@@ -342,7 +348,7 @@ func TestPieChart(t *testing.T) {
func validatePieChartRender(t *testing.T, p *Painter, opt PieChartOption, expectedResult string) {
t.Helper()
- _, err := NewPieChart(p, opt).Render()
+ err := p.PieChart(opt)
require.NoError(t, err)
data, err := p.Bytes()
require.NoError(t, err)
diff --git a/radar_chart.go b/radar_chart.go
index 0e684fe..3b9d7c8 100644
--- a/radar_chart.go
+++ b/radar_chart.go
@@ -24,6 +24,17 @@ type RadarIndicator struct {
Min float64
}
+// NewRadarChartOptionWithData returns an initialized RadarChartOption with the SeriesList set for the provided data slice.
+func NewRadarChartOptionWithData(data [][]float64, names []string, values []float64) RadarChartOption {
+ return RadarChartOption{
+ SeriesList: NewSeriesListRadar(data),
+ RadarIndicators: NewRadarIndicators(names, values),
+ Padding: defaultPadding,
+ Theme: GetDefaultTheme(),
+ Font: GetDefaultFont(),
+ }
+}
+
type RadarChartOption struct {
// Theme specifies the colors used for the pie chart.
Theme ColorPalette
@@ -58,8 +69,8 @@ func NewRadarIndicators(names []string, values []float64) []RadarIndicator {
return indicators
}
-// NewRadarChart returns a radar chart renderer
-func NewRadarChart(p *Painter, opt RadarChartOption) *radarChart {
+// newRadarChart returns a radar chart renderer.
+func newRadarChart(p *Painter, opt RadarChartOption) *radarChart {
return &radarChart{
p: p,
opt: &opt,
@@ -76,8 +87,8 @@ func (r *radarChart) render(result *defaultRenderResult, seriesList SeriesList)
maxValues := make([]float64, len(indicators))
for _, series := range seriesList {
for index, item := range series.Data {
- if index < len(maxValues) && item.Value > maxValues[index] {
- maxValues[index] = item.Value
+ if index < len(maxValues) && item > maxValues[index] {
+ maxValues[index] = item
}
}
}
@@ -106,30 +117,26 @@ func (r *radarChart) render(result *defaultRenderResult, seriesList SeriesList)
divideRadius := float64(int(radius / float64(divideCount)))
radius = divideRadius * float64(divideCount)
- seriesPainter.OverrideDrawingStyle(chartdraw.Style{
- StrokeColor: theme.GetAxisSplitLineColor(),
- StrokeWidth: 1,
- })
center := Point{X: cx, Y: cy}
for i := 0; i < divideCount; i++ {
- seriesPainter.Polygon(center, divideRadius*float64(i+1), sides)
+ seriesPainter.Polygon(center, divideRadius*float64(i+1), sides, theme.GetAxisSplitLineColor(), 1)
}
points := getPolygonPoints(center, radius, sides)
for _, p := range points {
- seriesPainter.MoveTo(center.X, center.Y)
- seriesPainter.LineTo(p.X, p.Y)
- seriesPainter.Stroke()
+ seriesPainter.moveTo(center.X, center.Y)
+ seriesPainter.lineTo(p.X, p.Y)
+ seriesPainter.stroke(theme.GetAxisSplitLineColor(), 1)
}
- seriesPainter.OverrideFontStyle(FontStyle{
+ fontStyle := FontStyle{
FontColor: theme.GetTextColor(),
FontSize: labelFontSize,
Font: opt.Font,
- })
+ }
offset := 5
// text generation
for index, p := range points {
name := indicators[index].Name
- b := seriesPainter.MeasureText(name)
+ b := seriesPainter.MeasureText(name, 0, fontStyle)
isXCenter := p.X == center.X
isYCenter := p.Y == center.Y
isRight := p.X > center.X
@@ -161,7 +168,7 @@ func (r *radarChart) render(result *defaultRenderResult, seriesList SeriesList)
if isLeft {
x -= b.Width() + offset
}
- seriesPainter.Text(name, x, y)
+ seriesPainter.Text(name, x, y, 0, fontStyle)
}
// radar chart
@@ -177,7 +184,7 @@ func (r *radarChart) render(result *defaultRenderResult, seriesList SeriesList)
var percent float64
offset := indicator.Max - indicator.Min
if offset > 0 {
- percent = (item.Value - indicator.Min) / offset
+ percent = (item - indicator.Min) / offset
}
r := percent * radius
p := getPolygonPoint(center, r, angles[j])
@@ -189,28 +196,15 @@ func (r *radarChart) render(result *defaultRenderResult, seriesList SeriesList)
dotFillColor = color
}
linePoints = append(linePoints, linePoints[0])
- seriesPainter.OverrideDrawingStyle(chartdraw.Style{
- StrokeColor: color,
- StrokeWidth: defaultStrokeWidth,
- DotWidth: defaultDotWidth,
- DotColor: color,
- FillColor: color.WithAlpha(20),
- })
- seriesPainter.LineStroke(linePoints).
- FillArea(linePoints)
- dotWith := 2.0
- seriesPainter.OverrideDrawingStyle(chartdraw.Style{
- StrokeWidth: defaultStrokeWidth,
- StrokeColor: color,
- FillColor: dotFillColor,
- })
+ seriesPainter.LineStroke(linePoints, color, defaultStrokeWidth)
+ seriesPainter.FillArea(linePoints, color.WithAlpha(20))
+ dotWith := defaultDotWidth
for index, point := range linePoints {
- seriesPainter.Circle(dotWith, point.X, point.Y)
- seriesPainter.FillStroke()
- if series.Label.Show && index < len(series.Data) {
- value := humanize.FtoaWithDigits(series.Data[index].Value, 2)
- b := seriesPainter.MeasureText(value)
- seriesPainter.Text(value, point.X-b.Width()/2, point.Y)
+ seriesPainter.Circle(dotWith, point.X, point.Y, dotFillColor, color, defaultStrokeWidth)
+ if flagIs(true, series.Label.Show) && index < len(series.Data) {
+ value := humanize.FtoaWithDigits(series.Data[index], 2)
+ b := seriesPainter.MeasureText(value, 0, fontStyle)
+ seriesPainter.Text(value, point.X-b.Width()/2, point.Y, 0, fontStyle)
}
}
}
@@ -226,19 +220,19 @@ func (r *radarChart) Render() (Box, error) {
}
renderResult, err := defaultRender(p, defaultRenderOption{
- Theme: opt.Theme,
- Padding: opt.Padding,
- SeriesList: opt.SeriesList,
- XAxis: XAxisOption{
+ theme: opt.Theme,
+ padding: opt.Padding,
+ seriesList: opt.SeriesList,
+ xAxis: &XAxisOption{
Show: False(),
},
- YAxis: []YAxisOption{
+ yAxis: []YAxisOption{
{
Show: False(),
},
},
- Title: opt.Title,
- Legend: opt.Legend,
+ title: opt.Title,
+ legend: &r.opt.Legend,
backgroundIsFilled: opt.backgroundIsFilled,
})
if err != nil {
diff --git a/radar_chart_test.go b/radar_chart_test.go
index b5fae3f..f9ab8e1 100644
--- a/radar_chart_test.go
+++ b/radar_chart_test.go
@@ -4,6 +4,7 @@ import (
"strconv"
"testing"
+ "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -13,7 +14,7 @@ func makeBasicRadarChartOption() RadarChartOption {
{5000, 14000, 28000, 26000, 42000, 21000},
}
return RadarChartOption{
- SeriesList: NewSeriesListDataFromValues(values, ChartTypeRadar),
+ SeriesList: NewSeriesListRadar(values),
Title: TitleOption{
Text: "Basic Radar Chart",
},
@@ -35,6 +36,31 @@ func makeBasicRadarChartOption() RadarChartOption {
}
}
+func TestNewRadarChartOptionWithData(t *testing.T) {
+ t.Parallel()
+
+ opt := NewRadarChartOptionWithData([][]float64{
+ {4200, 3000, 20000, 35000, 50000, 18000},
+ {5000, 14000, 28000, 26000, 42000, 21000},
+ }, []string{
+ "Sales",
+ "Administration",
+ "Information Technology",
+ "Customer Support",
+ "Development",
+ "Marketing",
+ }, []float64{
+ 6500, 16000, 30000, 38000, 52000, 25000,
+ })
+
+ assert.Len(t, opt.SeriesList, 2)
+ assert.Equal(t, ChartTypeRadar, opt.SeriesList[0].Type)
+ assert.Equal(t, defaultPadding, opt.Padding)
+
+ p := NewPainter(PainterOptions{})
+ assert.NoError(t, p.RadarChart(opt))
+}
+
func TestRadarChart(t *testing.T) {
t.Parallel()
@@ -48,13 +74,13 @@ func TestRadarChart(t *testing.T) {
name: "default",
defaultTheme: true,
makeOptions: makeBasicRadarChartOption,
- result: "",
+ result: "",
},
{
name: "themed",
defaultTheme: false,
makeOptions: makeBasicRadarChartOption,
- result: "",
+ result: "",
},
}
@@ -66,8 +92,7 @@ func TestRadarChart(t *testing.T) {
}
if tt.defaultTheme {
t.Run(strconv.Itoa(i)+"-"+tt.name, func(t *testing.T) {
- p, err := NewPainter(painterOptions)
- require.NoError(t, err)
+ p := NewPainter(painterOptions)
validateRadarChartRender(t, p.Child(PainterPaddingOption(Box{
Left: 20,
@@ -78,14 +103,12 @@ func TestRadarChart(t *testing.T) {
})
} else {
t.Run(strconv.Itoa(i)+"-"+tt.name+"-painter", func(t *testing.T) {
- p, err := NewPainter(painterOptions, PainterThemeOption(GetTheme(ThemeVividDark)))
- require.NoError(t, err)
+ p := NewPainter(painterOptions, PainterThemeOption(GetTheme(ThemeVividDark)))
validateRadarChartRender(t, p, tt.makeOptions(), tt.result)
})
t.Run(strconv.Itoa(i)+"-"+tt.name+"-options", func(t *testing.T) {
- p, err := NewPainter(painterOptions)
- require.NoError(t, err)
+ p := NewPainter(painterOptions)
opt := tt.makeOptions()
opt.Theme = GetTheme(ThemeVividDark)
@@ -98,7 +121,7 @@ func TestRadarChart(t *testing.T) {
func validateRadarChartRender(t *testing.T, p *Painter, opt RadarChartOption, expectedResult string) {
t.Helper()
- _, err := NewRadarChart(p, opt).Render()
+ err := p.RadarChart(opt)
require.NoError(t, err)
data, err := p.Bytes()
require.NoError(t, err)
diff --git a/range.go b/range.go
index a892cda..4dab171 100644
--- a/range.go
+++ b/range.go
@@ -11,22 +11,28 @@ const rangeMaxPaddingPercentMax = 20.0
const zeroSpanAdjustment = 1 // Adjustment
type axisRange struct {
- p *Painter
- divideCount int
- min float64
- max float64
- size int
+ p *Painter
+ valueFormatter ValueFormatter
+ divideCount int
+ min float64
+ max float64
+ size int
}
-// NewRange returns a range of data for an axis, this range will have padding to better present the data.
-func NewRange(painter *Painter, size, divideCount int, min, max, minPaddingScale, maxPaddingScale float64) axisRange {
+// newRange returns a range of data for an axis, this range will have padding to better present the data.
+func newRange(painter *Painter, valueFormatter ValueFormatter,
+ size, divideCount int, min, max, minPaddingScale, maxPaddingScale float64) axisRange {
min, max = padRange(divideCount, min, max, minPaddingScale, maxPaddingScale)
+ if valueFormatter == nil {
+ valueFormatter = defaultValueFormatter
+ }
return axisRange{
- p: painter,
- divideCount: divideCount,
- min: min,
- max: max,
- size: size,
+ p: painter,
+ valueFormatter: valueFormatter,
+ divideCount: divideCount,
+ min: min,
+ max: max,
+ size: size,
}
}
@@ -159,14 +165,10 @@ func friendlyRound(val, increment, defaultMultiplier, minMultiplier, maxMultipli
// Values returns values of range
func (r axisRange) Values() []string {
offset := (r.max - r.min) / float64(r.divideCount-1)
- formatter := defaultValueFormatter
- if r.p != nil && r.p.valueFormatter != nil {
- formatter = r.p.valueFormatter
- }
values := make([]string, r.divideCount)
for i := 0; i < r.divideCount; i++ {
v := r.min + float64(i)*offset
- values[i] = formatter(v)
+ values[i] = r.valueFormatter(v)
}
return values
}
diff --git a/series.go b/series.go
index 39a8231..a135372 100644
--- a/series.go
+++ b/series.go
@@ -2,46 +2,32 @@ package charts
import (
"math"
+ "sort"
"strings"
"github.com/dustin/go-humanize"
)
-// TODO - reconsider struct for v0.4.0
-type SeriesData struct {
- // Value is the retained value for the data.
- Value float64
-}
-
-// NewSeriesListDataFromValues returns a series list
-func NewSeriesListDataFromValues(values [][]float64, chartType ...string) SeriesList {
+// newSeriesListFromValues returns a series list for the given values and chart type.
+func newSeriesListFromValues(values [][]float64, chartType string, label SeriesLabel, names []string,
+ radius string, markPoint SeriesMarkPoint, markLine SeriesMarkLine) SeriesList {
seriesList := make(SeriesList, len(values))
for index, value := range values {
- seriesList[index] = NewSeriesFromValues(value, chartType...)
- }
- return seriesList
-}
-
-// NewSeriesFromValues returns a series
-func NewSeriesFromValues(values []float64, chartType ...string) Series {
- s := Series{
- Data: NewSeriesDataFromValues(values),
- }
- if len(chartType) != 0 {
- s.Type = chartType[0]
- }
- return s
-}
-
-// NewSeriesDataFromValues return a series data
-func NewSeriesDataFromValues(values []float64) []SeriesData {
- data := make([]SeriesData, len(values))
- for index, value := range values {
- data[index] = SeriesData{
- Value: value,
+ s := Series{
+ index: index,
+ Data: value,
+ Type: chartType,
+ Label: label,
+ Radius: radius,
+ MarkPoint: markPoint,
+ MarkLine: markLine,
+ }
+ if index < len(names) {
+ s.Name = names[index]
}
+ seriesList[index] = s
}
- return data
+ return seriesList
}
type SeriesLabel struct {
@@ -52,8 +38,8 @@ type SeriesLabel struct {
Formatter string
// FontStyle specifies the font and style for the label.
FontStyle FontStyle
- // Show flag for label
- Show bool
+ // Show flag for label, if unset the behavior will be defaulted based on the chart.
+ Show *bool
// Distance to the host graphic element.
Distance int // TODO - do we want to replace with just Offset?
// Position defines the label position.
@@ -91,7 +77,7 @@ type Series struct {
// Type is the type of series, it can be "line", "bar" or "pie". Default value is "line".
Type string
// Data provides the series data list.
- Data []SeriesData
+ Data []float64
// YAxisIndex is the index for the axis, it must be 0 or 1.
YAxisIndex int
// Label provides the series labels.
@@ -104,10 +90,6 @@ type Series struct {
MarkPoint SeriesMarkPoint
// MarkLine provides a series for mark lines.
MarkLine SeriesMarkLine
- // Max value of series
- Min *float64
- // Min value of series
- Max *float64
}
// SeriesList is a list of series to be rendered on the chart, typically constructed using NewSeriesListLine,
@@ -134,36 +116,98 @@ func (sl SeriesList) Filter(chartType string) SeriesList {
return arr
}
-// GetMinMax get max and min value of series list
-func (sl SeriesList) GetMinMax(axisIndex int) (float64, float64) {
+func (sl SeriesList) getYAxisCount() int {
+ for _, series := range sl {
+ if series.YAxisIndex == 1 {
+ return 2
+ } else if series.YAxisIndex != 0 {
+ return -1
+ }
+ }
+ return 1
+}
+
+// GetMinMax get max and min value of series list for a given y-axis index (either 0 or 1).
+func (sl SeriesList) GetMinMax(yaxisIndex int) (float64, float64) {
min := math.MaxFloat64
max := -math.MaxFloat64
for _, series := range sl {
- if series.YAxisIndex != axisIndex {
+ if series.YAxisIndex != yaxisIndex {
continue
}
for _, item := range series.Data {
- if item.Value == GetNullValue() {
+ if item == GetNullValue() {
continue
}
- if item.Value > max {
- max = item.Value
+ if item > max {
+ max = item
}
- if item.Value < min {
- min = item.Value
+ if item < min {
+ min = item
}
}
}
return min, max
}
+// LineSeriesOption provides series customization for NewSeriesListLine.
+type LineSeriesOption struct {
+ Label SeriesLabel
+ Names []string
+ MarkPoint SeriesMarkPoint
+ MarkLine SeriesMarkLine
+}
+
+// NewSeriesListLine builds a SeriesList for a line chart. The first dimension of the values indicates the population
+// of the data, while the second dimension provides the samples for the population.
+func NewSeriesListLine(values [][]float64, opts ...LineSeriesOption) SeriesList {
+ var opt LineSeriesOption
+ if len(opts) != 0 {
+ opt = opts[0]
+ }
+ return newSeriesListFromValues(values, ChartTypeLine,
+ opt.Label, opt.Names, "", opt.MarkPoint, opt.MarkLine)
+}
+
+// BarSeriesOption provides series customization for NewSeriesListBar or NewSeriesListHorizontalBar.
+type BarSeriesOption struct {
+ Label SeriesLabel
+ Names []string
+ MarkPoint SeriesMarkPoint
+ MarkLine SeriesMarkLine
+}
+
+// NewSeriesListBar builds a SeriesList for a bar chart. The first dimension of the values indicates the population
+// of the data, while the second dimension provides the samples for the population (on the X-Axis).
+func NewSeriesListBar(values [][]float64, opts ...BarSeriesOption) SeriesList {
+ var opt BarSeriesOption
+ if len(opts) != 0 {
+ opt = opts[0]
+ }
+ return newSeriesListFromValues(values, ChartTypeBar,
+ opt.Label, opt.Names, "", opt.MarkPoint, opt.MarkLine)
+}
+
+// NewSeriesListHorizontalBar builds a SeriesList for a horizontal bar chart. Horizontal bar charts are unique in that
+// these Series can not be combined with any other chart type.
+func NewSeriesListHorizontalBar(values [][]float64, opts ...BarSeriesOption) SeriesList {
+ var opt BarSeriesOption
+ if len(opts) != 0 {
+ opt = opts[0]
+ }
+ return newSeriesListFromValues(values, ChartTypeHorizontalBar,
+ opt.Label, opt.Names, "", opt.MarkPoint, opt.MarkLine)
+}
+
+// PieSeriesOption provides series customization for NewSeriesListPie.
type PieSeriesOption struct {
Radius string
Label SeriesLabel
Names []string
}
-func NewPieSeriesList(values []float64, opts ...PieSeriesOption) SeriesList {
+// NewSeriesListPie builds a SeriesList for a pie chart.
+func NewSeriesListPie(values []float64, opts ...PieSeriesOption) SeriesList {
result := make([]Series, len(values))
var opt PieSeriesOption
if len(opts) != 0 {
@@ -175,12 +219,8 @@ func NewPieSeriesList(values []float64, opts ...PieSeriesOption) SeriesList {
name = opt.Names[index]
}
s := Series{
- Type: ChartTypePie,
- Data: []SeriesData{
- {
- Value: v,
- },
- },
+ Type: ChartTypePie,
+ Data: []float64{v},
Radius: opt.Radius,
Label: opt.Label,
Name: name,
@@ -190,48 +230,170 @@ func NewPieSeriesList(values []float64, opts ...PieSeriesOption) SeriesList {
return result
}
-// TODO - lower case field names
+// RadarSeriesOption provides series customization for NewSeriesListRadar.
+type RadarSeriesOption struct {
+ Label SeriesLabel
+ Names []string
+}
+
+// NewSeriesListRadar builds a SeriesList for a Radar chart.
+func NewSeriesListRadar(values [][]float64, opts ...RadarSeriesOption) SeriesList {
+ var opt RadarSeriesOption
+ if len(opts) != 0 {
+ opt = opts[0]
+ }
+ return newSeriesListFromValues(values, ChartTypeRadar,
+ opt.Label, opt.Names, "", SeriesMarkPoint{}, SeriesMarkLine{})
+}
+
+// FunnelSeriesOption provides series customization for NewSeriesListFunnel.
+type FunnelSeriesOption struct {
+ Label SeriesLabel
+ Names []string
+}
+
+// NewSeriesListFunnel builds a series list for funnel charts.
+func NewSeriesListFunnel(values []float64, opts ...FunnelSeriesOption) SeriesList {
+ var opt FunnelSeriesOption
+ if len(opts) != 0 {
+ opt = opts[0]
+ }
+ seriesList := make(SeriesList, len(values))
+ for index, value := range values {
+ name := ""
+ if index < len(opt.Names) {
+ name = opt.Names[index]
+ }
+ seriesList[index] = Series{
+ Data: []float64{value},
+ Type: ChartTypeFunnel,
+ Label: opt.Label,
+ Name: name,
+ }
+ }
+ return seriesList
+}
+
type seriesSummary struct {
- // The index of max value
+ // Max is the maximum value in the series.
+ Max float64
+ // MaxIndex is the index of the maximum value in the series. If the series is empty this value will be -1.
MaxIndex int
- // The max value
- MaxValue float64
- // The index of min value
+ // Min is the minimum value in the series.
+ Min float64
+ // MinIndex is the index of the minimum value in the series. If the series is empty this value will be -1.
MinIndex int
- // The min value
- MinValue float64
- // THe average value
- AverageValue float64
+ // Average is the mean of all values in the series.
+ Average float64
+ // Median is the middle value of the series when it is sorted in ascending order.
+ Median float64
+ // StandardDeviation is a measure of the amount of variation or dispersion of a set of values. A low standard
+ // deviation indicates that the values tend to be close to the mean of the set, while a high standard deviation
+ // indicates that the values are spread out over a wider range.
+ StandardDeviation float64
+ // Skewness measures the asymmetry of the distribution of values in the series around the mean. If skewness is zero,
+ // the data are perfectly symmetrical, although not necessarily normal. If skewness is positive, the data is skewed
+ // right, meaning that the right tail is longer or fatter than the left. If skewness is negative, the data is skewed
+ // left, meaning that the left tail is longer or fatter than the right.
+ Skewness float64
+ // Kurtosis is a measure of the "tailedness" of the probability distribution of a real-valued random variable.
+ // High kurtosis in a data set is an indicator of substantial outliers. A negative kurtosis indicates a relatively flat distribution.
+ Kurtosis float64
}
// Summary returns numeric summary of series values (population statistics).
func (s *Series) Summary() seriesSummary {
- minIndex := -1
- maxIndex := -1
+ n := float64(len(s.Data))
+ if n == 0 {
+ return seriesSummary{
+ MinIndex: -1,
+ MaxIndex: -1,
+ }
+ }
+
+ // Initialize tracking variables
+ var minIndex, maxIndex int
minValue := math.MaxFloat64
maxValue := -math.MaxFloat64
- sum := float64(0)
- for j, item := range s.Data {
- if item.Value < minValue {
- minIndex = j
- minValue = item.Value
+ // For sums of powers:
+ var sum, sumSq, sumCu, sumQd float64
+
+ // Single pass to gather everything we need
+ for i, x := range s.Data {
+ if x < minValue {
+ minValue = x
+ minIndex = i
}
- if item.Value > maxValue {
- maxIndex = j
- maxValue = item.Value
+ if x > maxValue {
+ maxValue = x
+ maxIndex = i
}
- sum += item.Value
+
+ sum += x
+ sumSq += x * x
+ sumCu += x * x * x
+ sumQd += x * x * x * x
+ }
+
+ // Compute average (mean)
+ mean := sum / n
+ // Compute population variance = E[X^2] - (E[X])^2
+ variance := sumSq/n - mean*mean
+ stdDev := math.Sqrt(variance)
+ // Compute median: copy the data and sort
+ sortedData := make([]float64, len(s.Data))
+ copy(sortedData, s.Data)
+ sort.Float64s(sortedData)
+ var median float64
+ mid := len(sortedData) / 2
+ if len(sortedData)%2 == 0 {
+ median = (sortedData[mid-1] + sortedData[mid]) / 2.0
+ } else {
+ median = sortedData[mid]
+ }
+
+ // Compute population skewness:
+ // thirdCentral = Σ x^3 - 3μΣ x^2 + 3μ^2Σ x - nμ^3
+ // skewness = thirdCentral / (n * σ^3)
+ var skewness float64
+ if stdDev != 0 { // zero stdDev will result in a divide by zero
+ thirdCentral := sumCu - 3*mean*sumSq + 3*mean*mean*sum - n*mean*mean*mean
+ skewness = thirdCentral / (n * stdDev * stdDev * stdDev)
}
+
+ // Compute population excess kurtosis:
+ // fourthCentral = Σ x^4
+ // - 4μΣ x^3
+ // + 6μ^2Σ x^2
+ // - 4μ^3Σ x
+ // + nμ^4
+ // kurtosis = (fourthCentral / (n * σ^4))
+ // We don't subtract 3 (excess kurtosis) in our implementation.
+ var kurtosis float64
+ if variance != 0 {
+ fourthCentral := sumQd -
+ 4*mean*sumCu +
+ 6*mean*mean*sumSq -
+ 4*mean*mean*mean*sum +
+ n*mean*mean*mean*mean
+
+ kurtosis = fourthCentral / (n * variance * variance)
+ } // else, all points might be the same => kurtosis is undefined
+
return seriesSummary{
- MaxIndex: maxIndex,
- MaxValue: maxValue,
- MinIndex: minIndex,
- MinValue: minValue,
- AverageValue: sum / float64(len(s.Data)),
+ Max: maxValue,
+ MaxIndex: maxIndex,
+ Min: minValue,
+ MinIndex: minIndex,
+ Average: mean,
+ Median: median,
+ StandardDeviation: stdDev,
+ Skewness: skewness,
+ Kurtosis: kurtosis,
}
}
-// Names returns the names of series list
+// Names returns the names of series list.
func (sl SeriesList) Names() []string {
names := make([]string, len(sl))
for index, s := range sl {
@@ -240,35 +402,32 @@ func (sl SeriesList) Names() []string {
return names
}
-// LabelFormatter label formatter
-type LabelFormatter func(index int, value float64, percent float64) string
-
-// NewPieLabelFormatter returns a pie label formatter
-func NewPieLabelFormatter(seriesNames []string, layout string) LabelFormatter {
+// labelFormatPie formats the value for a pie chart label.
+func labelFormatPie(seriesNames []string, layout string, index int, value float64, percent float64) string {
if len(layout) == 0 {
layout = "{b}: {d}"
}
- return NewLabelFormatter(seriesNames, layout)
+ return newLabelFormatter(seriesNames, layout)(index, value, percent)
}
-// NewFunnelLabelFormatter returns a funner label formatter
-func NewFunnelLabelFormatter(seriesNames []string, layout string) LabelFormatter {
+// labelFormatFunnel formats the value for a funnel chart label.
+func labelFormatFunnel(seriesNames []string, layout string, index int, value float64, percent float64) string {
if len(layout) == 0 {
layout = "{b}({d})"
}
- return NewLabelFormatter(seriesNames, layout)
+ return newLabelFormatter(seriesNames, layout)(index, value, percent)
}
-// NewValueLabelFormatter returns a value formatter
-func NewValueLabelFormatter(seriesNames []string, layout string) LabelFormatter {
+// labelFormatValue returns a formatted value.
+func labelFormatValue(seriesNames []string, layout string, index int, value float64, percent float64) string {
if len(layout) == 0 {
layout = "{c}"
}
- return NewLabelFormatter(seriesNames, layout)
+ return newLabelFormatter(seriesNames, layout)(index, value, percent)
}
-// NewLabelFormatter returns a label formatter
-func NewLabelFormatter(seriesNames []string, layout string) LabelFormatter {
+// newLabelFormatter returns a label formatter.
+func newLabelFormatter(seriesNames []string, layout string) func(index int, value float64, percent float64) string {
return func(index int, value, percent float64) string {
percentText := ""
if percent >= 0 {
diff --git a/series_label.go b/series_label.go
index b9b86a1..583bb00 100644
--- a/series_label.go
+++ b/series_label.go
@@ -14,18 +14,18 @@ type labelRenderValue struct {
Radians float64
}
-type LabelValue struct {
- Index int
- Value float64
- X int
- Y int
- Radians float64
- FontStyle FontStyle
- Vertical bool
- Offset OffsetInt
+type labelValue struct {
+ index int
+ value float64
+ x int
+ y int
+ radians float64
+ fontStyle FontStyle
+ vertical bool
+ offset OffsetInt
}
-type SeriesLabelPainter struct {
+type seriesLabelPainter struct {
p *Painter
seriesNames []string
label *SeriesLabel
@@ -34,62 +34,50 @@ type SeriesLabelPainter struct {
values []labelRenderValue
}
-type SeriesLabelPainterParams struct {
- P *Painter
- SeriesNames []string
- Label SeriesLabel
- Theme ColorPalette
- Font *truetype.Font
-}
-
-func NewSeriesLabelPainter(params SeriesLabelPainterParams) *SeriesLabelPainter {
- return &SeriesLabelPainter{
- p: params.P,
- seriesNames: params.SeriesNames,
- label: ¶ms.Label,
- theme: params.Theme,
- font: params.Font,
+func newSeriesLabelPainter(p *Painter, seriesNames []string, label SeriesLabel,
+ theme ColorPalette, font *truetype.Font) *seriesLabelPainter {
+ return &seriesLabelPainter{
+ p: p,
+ seriesNames: seriesNames,
+ label: &label,
+ theme: theme,
+ font: font,
values: make([]labelRenderValue, 0),
}
}
-func (o *SeriesLabelPainter) Add(value LabelValue) {
+func (o *seriesLabelPainter) Add(value labelValue) {
label := o.label
distance := label.Distance
if distance == 0 {
distance = 5
}
- text := NewValueLabelFormatter(o.seriesNames, label.Formatter)(value.Index, value.Value, -1)
+ text := labelFormatValue(o.seriesNames, label.Formatter, value.index, value.value, -1)
labelStyle := FontStyle{
FontColor: o.theme.GetTextColor(),
FontSize: labelFontSize,
- Font: getPreferredFont(label.FontStyle.Font, value.FontStyle.Font, o.font),
+ Font: getPreferredFont(label.FontStyle.Font, value.fontStyle.Font, o.font),
}
if label.FontStyle.FontSize != 0 {
labelStyle.FontSize = label.FontStyle.FontSize
- } else if value.FontStyle.FontSize != 0 {
- labelStyle.FontSize = value.FontStyle.FontSize
+ } else if value.fontStyle.FontSize != 0 {
+ labelStyle.FontSize = value.fontStyle.FontSize
}
if !label.FontStyle.FontColor.IsZero() {
labelStyle.FontColor = label.FontStyle.FontColor
- } else if !value.FontStyle.FontColor.IsZero() {
- labelStyle.FontColor = value.FontStyle.FontColor
+ } else if !value.fontStyle.FontColor.IsZero() {
+ labelStyle.FontColor = value.fontStyle.FontColor
}
p := o.p
- p.OverrideDrawingStyle(chartdraw.Style{FontStyle: labelStyle})
- rotated := value.Radians != 0
- if rotated {
- p.SetTextRotation(value.Radians)
- }
- textBox := p.MeasureText(text)
+ textBox := p.MeasureText(text, value.radians, labelStyle)
renderValue := labelRenderValue{
Text: text,
FontStyle: labelStyle,
- X: value.X,
- Y: value.Y,
- Radians: value.Radians,
+ X: value.x,
+ Y: value.y,
+ Radians: value.radians,
}
- if value.Vertical {
+ if value.vertical {
renderValue.X -= textBox.Width() >> 1
renderValue.Y -= distance
} else {
@@ -97,25 +85,17 @@ func (o *SeriesLabelPainter) Add(value LabelValue) {
renderValue.Y += textBox.Height() >> 1
renderValue.Y -= 2
}
- if rotated {
- renderValue.X = value.X + textBox.Width()>>1 - 1
- p.ClearTextRotation()
- } else if textBox.Width()%2 != 0 {
- renderValue.X++
+ if value.radians != 0 {
+ renderValue.X = value.x + (textBox.Width() >> 1) - 1
}
- renderValue.X += value.Offset.Left
- renderValue.Y += value.Offset.Top
+ renderValue.X += value.offset.Left
+ renderValue.Y += value.offset.Top
o.values = append(o.values, renderValue)
}
-func (o *SeriesLabelPainter) Render() (Box, error) {
+func (o *seriesLabelPainter) Render() (Box, error) {
for _, item := range o.values {
- o.p.OverrideFontStyle(item.FontStyle)
- if item.Radians != 0 {
- o.p.TextRotation(item.Text, item.X, item.Y, item.Radians)
- } else {
- o.p.Text(item.Text, item.X, item.Y)
- }
+ o.p.Text(item.Text, item.X, item.Y, item.Radians, item.FontStyle)
}
return chartdraw.BoxZero, nil
}
diff --git a/series_test.go b/series_test.go
index 3454ab1..2ebddd6 100644
--- a/series_test.go
+++ b/series_test.go
@@ -12,26 +12,24 @@ func TestNewSeriesListDataFromValues(t *testing.T) {
assert.Equal(t, SeriesList{
{
Type: ChartTypeBar,
- Data: []SeriesData{
- {
- Value: 1.0,
- },
+ Data: []float64{
+ 1.0,
},
},
- }, NewSeriesListDataFromValues([][]float64{
+ }, NewSeriesListBar([][]float64{
{
1,
},
- }, ChartTypeBar))
+ }))
}
func TestSeriesLists(t *testing.T) {
t.Parallel()
- seriesList := NewSeriesListDataFromValues([][]float64{
+ seriesList := NewSeriesListBar([][]float64{
{1, 2},
{10},
- }, ChartTypeBar)
+ })
assert.Equal(t, 2, len(seriesList.Filter(ChartTypeBar)))
assert.Equal(t, 0, len(seriesList.Filter(ChartTypeLine)))
@@ -39,26 +37,102 @@ func TestSeriesLists(t *testing.T) {
min, max := seriesList.GetMinMax(0)
assert.Equal(t, float64(10), max)
assert.Equal(t, float64(1), min)
+}
+
+func TestSeriesSummary(t *testing.T) {
+ t.Parallel()
+
+ seriesList := NewSeriesListLine([][]float64{
+ {10},
+ {1, 2},
+ {1, 2, 3},
+ {1, 2, 3, 4},
+ {3, 7, 11, 13},
+ })
- assert.Equal(t, seriesSummary{
- MaxIndex: 1,
- MaxValue: 2,
- MinIndex: 0,
- MinValue: 1,
- AverageValue: 1.5,
- }, seriesList[0].Summary())
+ t.Run("empty_series", func(t *testing.T) {
+ assert.Equal(t, seriesSummary{
+ MaxIndex: -1,
+ MinIndex: -1,
+ }, (&Series{}).Summary())
+ })
+ t.Run("one_value", func(t *testing.T) {
+ assert.Equal(t, seriesSummary{
+ Max: 10,
+ MaxIndex: 0,
+ Min: 10,
+ MinIndex: 0,
+ Average: 10,
+ Median: 10,
+ StandardDeviation: 0.0,
+ Skewness: 0.0,
+ Kurtosis: 0.0,
+ }, seriesList[0].Summary())
+ })
+ t.Run("two_values", func(t *testing.T) {
+ assert.Equal(t, seriesSummary{
+ Max: 2,
+ MaxIndex: 1,
+ Min: 1,
+ MinIndex: 0,
+ Average: 1.5,
+ Median: 1.5,
+ StandardDeviation: 0.5,
+ Skewness: 0.0,
+ Kurtosis: 1.0,
+ }, seriesList[1].Summary())
+ })
+ t.Run("three_values", func(t *testing.T) {
+ assert.Equal(t, seriesSummary{
+ Max: 3,
+ MaxIndex: 2,
+ Min: 1,
+ MinIndex: 0,
+ Average: 2,
+ Median: 2,
+ StandardDeviation: 0.8164965809277263,
+ Skewness: 0.0,
+ Kurtosis: 1.4999999999999987,
+ }, seriesList[2].Summary())
+ })
+ t.Run("four_values", func(t *testing.T) {
+ assert.Equal(t, seriesSummary{
+ Max: 4,
+ MaxIndex: 3,
+ Min: 1,
+ MinIndex: 0,
+ Average: 2.5,
+ Median: 2.5,
+ StandardDeviation: 1.118033988749895,
+ Skewness: 0.0,
+ Kurtosis: 1.64,
+ }, seriesList[3].Summary())
+ })
+ t.Run("prime_values", func(t *testing.T) {
+ assert.Equal(t, seriesSummary{
+ Max: 13,
+ MaxIndex: 3,
+ Min: 3,
+ MinIndex: 0,
+ Average: 8.5,
+ Median: 9,
+ StandardDeviation: 3.840572873934304,
+ Skewness: -0.2780305556539629,
+ Kurtosis: 1.5733984487216317,
+ }, seriesList[4].Summary())
+ })
}
func TestFormatter(t *testing.T) {
t.Parallel()
- assert.Equal(t, "a: 12%", NewPieLabelFormatter([]string{
+ assert.Equal(t, "a: 12%", labelFormatPie([]string{
"a",
"b",
- }, "")(0, 10, 0.12))
+ }, "", 0, 10, 0.12))
- assert.Equal(t, "10", NewValueLabelFormatter([]string{
+ assert.Equal(t, "10", labelFormatValue([]string{
"a",
"b",
- }, "")(0, 10, 0.12))
+ }, "", 0, 10, 0.12))
}
diff --git a/table.go b/table.go
index ba0f128..52201f7 100644
--- a/table.go
+++ b/table.go
@@ -7,13 +7,68 @@ import (
"github.com/go-analyze/charts/chartdraw/drawing"
)
+// TableOptionRenderDirect table render with the provided options directly to an image. Table options are different
+// from other charts as they include the state for initializing the Painter, where other charts accept the Painter. If
+// you want to write a Table on an existing Painter use TableOptionRender
+func TableOptionRenderDirect(opt TableChartOption) (*Painter, error) {
+ if opt.OutputFormat == "" {
+ opt.OutputFormat = chartDefaultOutputFormat
+ }
+ if opt.Width <= 0 {
+ opt.Width = defaultChartWidth
+ }
+
+ p := NewPainter(PainterOptions{
+ OutputFormat: opt.OutputFormat,
+ Width: opt.Width,
+ Height: 100, // is only used to calculate the height of the table
+ Font: opt.FontStyle.Font,
+ })
+ info, err := newTableChart(p, opt).render()
+ if err != nil {
+ return nil, err
+ }
+
+ p = NewPainter(PainterOptions{
+ OutputFormat: opt.OutputFormat,
+ Width: info.width,
+ Height: info.height,
+ Font: opt.FontStyle.Font,
+ })
+ if _, err = newTableChart(p, opt).renderWithInfo(info); err != nil {
+ return nil, err
+ }
+ return p, nil
+}
+
+// TableRenderValues renders a table chart with the simple header and data values provided.
+func TableRenderValues(header []string, data [][]string, spanMaps ...map[int]int) (*Painter, error) {
+ opt := TableChartOption{
+ Header: header,
+ Data: data,
+ }
+ if len(spanMaps) != 0 {
+ spanMap := spanMaps[0]
+ spans := make([]int, len(opt.Header))
+ for index := range spans {
+ v, ok := spanMap[index]
+ if !ok {
+ v = 1
+ }
+ spans[index] = v
+ }
+ opt.Spans = spans
+ }
+ return TableOptionRenderDirect(opt)
+}
+
type tableChart struct {
p *Painter
opt *TableChartOption
}
-// NewTableChart returns a table chart render
-func NewTableChart(p *Painter, opt TableChartOption) *tableChart {
+// newTableChart returns a table chart render.
+func newTableChart(p *Painter, opt TableChartOption) *tableChart {
return &tableChart{
p: p,
opt: &opt,
@@ -163,13 +218,10 @@ func (t *tableChart) render() (*renderInfo, error) {
info.columnWidths = columnWidths
height := 0
- style := chartdraw.Style{
- FontStyle: FontStyle{
- FontSize: fontStyle.FontSize,
- FontColor: opt.HeaderFontColor,
- Font: fontStyle.Font,
- },
- FillColor: opt.HeaderFontColor,
+ headerFontStyle := FontStyle{
+ FontSize: fontStyle.FontSize,
+ FontColor: opt.HeaderFontColor,
+ Font: fontStyle.Font,
}
// textAligns := opt.TextAligns
@@ -182,7 +234,8 @@ func (t *tableChart) render() (*renderInfo, error) {
// processing of the table cells
renderTableCells := func(
- style chartdraw.Style,
+ fontStyle FontStyle,
+ fillColor Color,
rowIndex int,
textList []string,
currentHeight int,
@@ -197,23 +250,22 @@ func (t *tableChart) render() (*renderInfo, error) {
Text: text,
Row: rowIndex,
Column: index,
- FontStyle: style.FontStyle,
- FillColor: style.FillColor,
+ FontStyle: fontStyle,
+ FillColor: fillColor,
}
if opt.CellModifier != nil {
tc = opt.CellModifier(tc)
// Update style values to capture any changes
- style.FontStyle = tc.FontStyle
- style.FillColor = tc.FillColor
+ fontStyle = tc.FontStyle
+ fillColor = tc.FillColor
}
cells[index] = tc
- p.SetStyle(style)
x := values[index]
y := currentHeight + cellPadding.Top
width := values[index+1] - x
x += cellPadding.Left
width -= paddingWidth
- box := p.TextFit(text, x, y+int(fontStyle.FontSize), width, getTextAlign(index))
+ box := p.TextFit(text, x, y+int(fontStyle.FontSize), width, fontStyle, getTextAlign(index))
// calculate the highest height
if box.Height()+paddingHeight > cellMaxHeight {
cellMaxHeight = box.Height() + paddingHeight
@@ -225,16 +277,15 @@ func (t *tableChart) render() (*renderInfo, error) {
info.tableCells = make([][]TableCell, len(opt.Data)+1)
// processing of the table headers
- headerCells, headerHeight := renderTableCells(style, 0, opt.Header, height, opt.Padding)
+ headerCells, headerHeight := renderTableCells(headerFontStyle, opt.HeaderFontColor,
+ 0, opt.Header, height, opt.Padding)
info.tableCells[0] = headerCells
height += headerHeight
info.headerHeight = headerHeight
// processing of the table contents
- style.FontColor = fontStyle.FontColor
- style.FillColor = fontStyle.FontColor
for index, textList := range opt.Data {
- newCells, cellHeight := renderTableCells(style, index+1, textList, height, opt.Padding)
+ newCells, cellHeight := renderTableCells(fontStyle, fontStyle.FontColor, index+1, textList, height, opt.Padding)
info.tableCells[index+1] = newCells
info.rowHeights = append(info.rowHeights, cellHeight)
height += cellHeight
@@ -262,7 +313,7 @@ func (t *tableChart) renderWithInfo(info *renderInfo) (Box, error) {
opt.HeaderBackgroundColor = tableLightThemeSetting.headerColor
}
}
- p.SetBackground(info.width, info.headerHeight, opt.HeaderBackgroundColor, true)
+ p.SetBackground(info.width, info.headerHeight, opt.HeaderBackgroundColor)
if opt.RowBackgroundColors == nil {
if opt.Theme.IsDark() {
@@ -279,7 +330,7 @@ func (t *tableChart) renderWithInfo(info *renderInfo) (Box, error) {
Top: currentHeight,
IsSet: true,
}))
- child.SetBackground(p.Width(), h, color, true)
+ child.SetBackground(p.Width(), h, color)
currentHeight += h
}
// adjust the background color according to the set table style
@@ -304,7 +355,7 @@ func (t *tableChart) renderWithInfo(info *renderInfo) (Box, error) {
}))
w := info.columnWidths[j] - padding.Left - padding.Top
h := heights[i] - padding.Top - padding.Bottom
- child.SetBackground(w, h, tc.FillColor, true)
+ child.SetBackground(w, h, tc.FillColor)
}
left += info.columnWidths[j]
}
@@ -334,11 +385,7 @@ func (t *tableChart) Render() (Box, error) {
if p.outputFormat == ChartOutputSVG {
fn = chartdraw.SVG
}
- newRender, err := fn(p.Width(), 100)
- if err != nil {
- return BoxZero, err
- }
- p.render = newRender
+ p.render = fn(p.Width(), 100)
info, err := t.render()
if err != nil {
return BoxZero, err
diff --git a/table_test.go b/table_test.go
index 78a5f41..877e011 100644
--- a/table_test.go
+++ b/table_test.go
@@ -71,13 +71,13 @@ func TestTableChart(t *testing.T) {
}
return opt
},
- result: "",
+ result: "",
},
{
name: "dark_theme",
theme: GetTheme(ThemeVividDark),
makeOptions: makeDefaultTableChartOptions,
- result: "",
+ result: "",
},
{
name: "cell_modified",
@@ -98,7 +98,7 @@ func TestTableChart(t *testing.T) {
}
return opt
},
- result: "",
+ result: "",
},
{
name: "error_no_header",
@@ -117,11 +117,10 @@ func TestTableChart(t *testing.T) {
Width: 600,
Height: 400,
}
- runName := strconv.Itoa(i)
+ runName := strconv.Itoa(i) + "-" + tt.name
if tt.theme != nil {
t.Run(runName+"-theme_painter", func(t *testing.T) {
- p, err := NewPainter(painterOptions, PainterThemeOption(tt.theme))
- require.NoError(t, err)
+ p := NewPainter(painterOptions, PainterThemeOption(tt.theme))
opt := tt.makeOptions()
validateTableChartRender(t, p, opt, tt.result, tt.errorExpected)
@@ -129,8 +128,7 @@ func TestTableChart(t *testing.T) {
runName += "-theme_opt"
}
t.Run(runName, func(t *testing.T) {
- p, err := NewPainter(painterOptions)
- require.NoError(t, err)
+ p := NewPainter(painterOptions)
opt := tt.makeOptions()
opt.Theme = tt.theme
@@ -143,7 +141,7 @@ func validateTableChartRender(t *testing.T, p *Painter, opt TableChartOption,
expectedResult string, errorExpected bool) {
t.Helper()
- _, err := NewTableChart(p, opt).Render()
+ err := p.TableChart(opt)
if errorExpected {
require.Error(t, err)
return
diff --git a/theme_test.go b/theme_test.go
index 6385111..f0a4b76 100644
--- a/theme_test.go
+++ b/theme_test.go
@@ -55,12 +55,11 @@ func renderTestLineChartWithThemeName(t *testing.T, fullChart bool, themeName st
func renderTestLineChartWithTheme(t *testing.T, fullChart bool, theme ColorPalette) []byte {
t.Helper()
- p, err := NewPainter(PainterOptions{
+ p := NewPainter(PainterOptions{
OutputFormat: ChartOutputSVG,
Width: 600,
Height: 400,
})
- require.NoError(t, err)
opt := makeFullLineChartOption()
if len(opt.YAxis) == 0 {
opt.YAxis = []YAxisOption{{}}
@@ -73,7 +72,7 @@ func renderTestLineChartWithTheme(t *testing.T, fullChart bool, theme ColorPalet
}
opt.Theme = theme
- _, err = NewLineChart(p, opt).Render()
+ err := p.LineChart(opt)
require.NoError(t, err)
data, err := p.Bytes()
require.NoError(t, err)
@@ -84,42 +83,42 @@ func TestThemeLight(t *testing.T) {
t.Parallel()
svg := renderTestLineChartWithThemeName(t, true, ThemeLight)
- assertEqualSVG(t, "", svg)
+ assertEqualSVG(t, "", svg)
}
func TestThemeDark(t *testing.T) {
t.Parallel()
svg := renderTestLineChartWithThemeName(t, true, ThemeDark)
- assertEqualSVG(t, "", svg)
+ assertEqualSVG(t, "", svg)
}
func TestThemeVividLight(t *testing.T) {
t.Parallel()
svg := renderTestLineChartWithThemeName(t, true, ThemeVividLight)
- assertEqualSVG(t, "", svg)
+ assertEqualSVG(t, "", svg)
}
func TestThemeVividDark(t *testing.T) {
t.Parallel()
svg := renderTestLineChartWithThemeName(t, true, ThemeVividDark)
- assertEqualSVG(t, "", svg)
+ assertEqualSVG(t, "", svg)
}
func TestThemeAnt(t *testing.T) {
t.Parallel()
svg := renderTestLineChartWithThemeName(t, true, ThemeAnt)
- assertEqualSVG(t, "", svg)
+ assertEqualSVG(t, "", svg)
}
func TestThemeGrafana(t *testing.T) {
t.Parallel()
svg := renderTestLineChartWithThemeName(t, true, ThemeGrafana)
- assertEqualSVG(t, "", svg)
+ assertEqualSVG(t, "", svg)
}
func TestLightThemeSeriesRepeat(t *testing.T) {
@@ -138,7 +137,7 @@ func TestLightThemeSeriesRepeat(t *testing.T) {
{R: 200, G: 50, B: 50, A: 255},
},
})
- assertEqualSVG(t, "", svg)
+ assertEqualSVG(t, "", svg)
}
func TestDarkThemeSeriesRepeat(t *testing.T) {
@@ -157,7 +156,7 @@ func TestDarkThemeSeriesRepeat(t *testing.T) {
{R: 200, G: 50, B: 50, A: 255},
},
})
- assertEqualSVG(t, "", svg)
+ assertEqualSVG(t, "", svg)
}
func TestWithAxisColor(t *testing.T) {
diff --git a/title.go b/title.go
index 7f4e485..e36f629 100644
--- a/title.go
+++ b/title.go
@@ -47,8 +47,8 @@ type titlePainter struct {
opt *TitleOption
}
-// NewTitlePainter returns a title renderer
-func NewTitlePainter(p *Painter, opt TitleOption) *titlePainter {
+// newTitlePainter returns a title renderer
+func newTitlePainter(p *Painter, opt TitleOption) *titlePainter {
return &titlePainter{
p: p,
opt: &opt,
@@ -111,8 +111,7 @@ func (t *titlePainter) Render() (Box, error) {
textMaxHeight := 0
textTotalHeight := 0
for index, item := range measureOptions {
- p.OverrideFontStyle(item.style)
- textBox := p.MeasureText(item.text)
+ textBox := p.MeasureText(item.text, 0, item.style)
w := textBox.Width()
h := textBox.Height()
@@ -159,10 +158,9 @@ func (t *titlePainter) Render() (Box, error) {
}
startY := titleY
for _, item := range measureOptions {
- p.OverrideFontStyle(item.style)
x := titleX + (textMaxWidth-item.width)>>1
y := titleY + item.height
- p.Text(item.text, x, y)
+ p.Text(item.text, x, y, 0, item.style)
titleY = y
}
diff --git a/title_test.go b/title_test.go
index be51c15..6bfea5b 100644
--- a/title_test.go
+++ b/title_test.go
@@ -20,7 +20,7 @@ func TestTitleRenderer(t *testing.T) {
{
name: "no_content",
render: func(p *Painter) ([]byte, error) {
- _, err := NewTitlePainter(p, TitleOption{
+ _, err := newTitlePainter(p, TitleOption{
Text: "",
Subtext: "",
}).Render()
@@ -34,7 +34,7 @@ func TestTitleRenderer(t *testing.T) {
{
name: "offset_number",
render: func(p *Painter) ([]byte, error) {
- _, err := NewTitlePainter(p, TitleOption{
+ _, err := newTitlePainter(p, TitleOption{
Text: "title",
Subtext: "subTitle",
Offset: OffsetStr{
@@ -47,12 +47,12 @@ func TestTitleRenderer(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "offset_percent",
render: func(p *Painter) ([]byte, error) {
- _, err := NewTitlePainter(p, TitleOption{
+ _, err := newTitlePainter(p, TitleOption{
Text: "title",
Subtext: "subTitle",
Offset: OffsetStr{
@@ -65,12 +65,12 @@ func TestTitleRenderer(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "offset_right",
render: func(p *Painter) ([]byte, error) {
- _, err := NewTitlePainter(p, TitleOption{
+ _, err := newTitlePainter(p, TitleOption{
Text: "title",
Subtext: "subTitle",
Offset: OffsetRight,
@@ -80,12 +80,12 @@ func TestTitleRenderer(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "offset_center",
render: func(p *Painter) ([]byte, error) {
- _, err := NewTitlePainter(p, TitleOption{
+ _, err := newTitlePainter(p, TitleOption{
Text: "title",
Subtext: "subTitle",
Offset: OffsetCenter,
@@ -95,12 +95,12 @@ func TestTitleRenderer(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "offset_bottom",
render: func(p *Painter) ([]byte, error) {
- _, err := NewTitlePainter(p, TitleOption{
+ _, err := newTitlePainter(p, TitleOption{
Text: "title",
Subtext: "subTitle",
Offset: OffsetStr{
@@ -112,12 +112,12 @@ func TestTitleRenderer(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "offset_bottom_right",
render: func(p *Painter) ([]byte, error) {
- _, err := NewTitlePainter(p, TitleOption{
+ _, err := newTitlePainter(p, TitleOption{
Text: "title",
Subtext: "subTitle",
Offset: OffsetStr{
@@ -130,12 +130,12 @@ func TestTitleRenderer(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "offset_bottom_center",
render: func(p *Painter) ([]byte, error) {
- _, err := NewTitlePainter(p, TitleOption{
+ _, err := newTitlePainter(p, TitleOption{
Text: "title",
Subtext: "subTitle",
Offset: OffsetStr{
@@ -148,12 +148,12 @@ func TestTitleRenderer(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
{
name: "custom_font",
render: func(p *Painter) ([]byte, error) {
- _, err := NewTitlePainter(p, TitleOption{
+ _, err := newTitlePainter(p, TitleOption{
Text: "title",
Subtext: "subTitle",
FontStyle: FontStyle{
@@ -170,18 +170,17 @@ func TestTitleRenderer(t *testing.T) {
}
return p.Bytes()
},
- result: "",
+ result: "",
},
}
for i, tt := range tests {
t.Run(strconv.Itoa(i)+"-"+tt.name, func(t *testing.T) {
- p, err := NewPainter(PainterOptions{
+ p := NewPainter(PainterOptions{
OutputFormat: ChartOutputSVG,
Width: 600,
Height: 400,
}, PainterThemeOption(GetTheme(ThemeLight)))
- require.NoError(t, err)
data, err := tt.render(p)
require.NoError(t, err)
assertEqualSVG(t, tt.result, data)
diff --git a/util.go b/util.go
index 36824ce..b88be22 100644
--- a/util.go
+++ b/util.go
@@ -37,6 +37,7 @@ func flagIs(is bool, flag *bool) bool {
return *flag == is
}
+// TODO - replace when we support a newer version of go
func containsInt(values []int, value int) bool {
for _, v := range values {
if v == value {
@@ -111,12 +112,14 @@ func sumInt(values []int) int {
return sum
}
+// TODO - replace when we support a newer version of go
func reverseStringSlice(stringList []string) {
for i, j := 0, len(stringList)-1; i < j; i, j = i+1, j-1 {
stringList[i], stringList[j] = stringList[j], stringList[i]
}
}
+// TODO - replace when we support a newer version of go
func reverseIntSlice(intList []int) {
for i, j := 0, len(intList)-1; i < j; i, j = i+1, j-1 {
intList[i], intList[j] = intList[j], intList[i]
@@ -152,7 +155,7 @@ const gValue = mValue * kValue
const tValue = gValue * kValue
// FormatValueHumanizeShort takes in a value and a specified precision, rounding to the specified precision and
-// returning a human friendly number string including commas. If the value is over 1,000 it will be reduced to a
+// returning a human friendly number string including commas. If the value is over 1,000 it will be reduced to a
// shorter version with the appropriate k, M, G, T suffix.
func FormatValueHumanizeShort(value float64, decimals int, ensureTrailingZeros bool) string {
if value >= tValue {
diff --git a/xaxis.go b/xaxis.go
index baaa684..86c9d86 100644
--- a/xaxis.go
+++ b/xaxis.go
@@ -21,9 +21,11 @@ type XAxisOption struct {
TextRotation float64
// LabelOffset is the offset of each label.
LabelOffset OffsetInt
+ // ValueFormatter defines how float values should be rendered to strings, notably for numeric axis labels.
+ ValueFormatter ValueFormatter
// Unit is a suggestion for how large the axis step is, this is a recommendation only. Larger numbers result in fewer labels.
Unit float64
- // LabelCount is the number of labels to show on the axis. Specify a smaller number to reduce writing collisions.
+ // LabelCount is the number of labels to show on the axis. Specify a smaller number to reduce writing collisions.
LabelCount int
// LabelCountAdjustment specifies a relative influence on how many labels should be rendered.
// Typically, this is negative to result in cleaner graphs, positive values may result in text collisions.
@@ -34,12 +36,12 @@ type XAxisOption struct {
const defaultXAxisHeight = 30
const boundaryGapDefaultThreshold = 40
-func (opt *XAxisOption) ToAxisOption() AxisOption {
+func (opt *XAxisOption) toAxisOption() axisOption {
position := PositionBottom
if opt.Position == PositionTop {
position = PositionTop
}
- axisOpt := AxisOption{
+ axisOpt := axisOption{
Theme: opt.Theme,
Data: opt.Data,
DataStartIndex: opt.DataStartIndex,
@@ -61,7 +63,7 @@ func (opt *XAxisOption) ToAxisOption() AxisOption {
return axisOpt
}
-// NewBottomXAxis returns a bottom x axis renderer
-func NewBottomXAxis(p *Painter, opt XAxisOption) *axisPainter {
- return NewAxisPainter(p, opt.ToAxisOption())
+// newBottomXAxis returns a bottom x-axis renderer.
+func newBottomXAxis(p *Painter, opt XAxisOption) *axisPainter {
+ return newAxisPainter(p, opt.toAxisOption())
}
diff --git a/yaxis.go b/yaxis.go
index 783961a..3398500 100644
--- a/yaxis.go
+++ b/yaxis.go
@@ -23,7 +23,7 @@ type YAxisOption struct {
Formatter string
// Unit is a suggestion for how large the axis step is, this is a recommendation only. Larger numbers result in fewer labels.
Unit float64
- // LabelCount is the number of labels to show on the axis. Specify a smaller number to reduce writing collisions.
+ // LabelCount is the number of labels to show on the axis. Specify a smaller number to reduce writing collisions.
LabelCount int
// LabelCountAdjustment specifies a relative influence on how many labels should be rendered.
// Typically, this is negative to result in cleaner graphs, positive values may result in text collisions.
@@ -36,18 +36,17 @@ type YAxisOption struct {
// SpineLineShow can be set to enforce if the vertical spine on the axis should be shown or not.
// By default, not shown unless a category axis.
SpineLineShow *bool
+ // ValueFormatter defines how float values should be rendered to strings, notably for numeric axis labels.
+ ValueFormatter ValueFormatter
}
-func (opt *YAxisOption) ToAxisOption(p *Painter) AxisOption {
+func (opt *YAxisOption) toAxisOption(fallbackTheme ColorPalette) axisOption {
position := PositionLeft
if opt.Position == PositionRight {
position = PositionRight
}
- theme := opt.Theme
- if theme == nil {
- theme = p.theme
- }
- axisOpt := AxisOption{
+ theme := getPreferredTheme(opt.Theme, fallbackTheme)
+ axisOpt := axisOption{
Formatter: opt.Formatter,
Theme: theme,
Data: opt.Data,
@@ -86,21 +85,21 @@ func (opt *YAxisOption) ToAxisOption(p *Painter) AxisOption {
return axisOpt
}
-// NewLeftYAxis returns a left y axis renderer
-func NewLeftYAxis(p *Painter, opt YAxisOption) *axisPainter {
+// newLeftYAxis returns a left y-axis renderer.
+func newLeftYAxis(p *Painter, opt YAxisOption) *axisPainter {
p = p.Child(PainterPaddingOption(Box{
Bottom: defaultXAxisHeight,
}))
- return NewAxisPainter(p, opt.ToAxisOption(p))
+ return newAxisPainter(p, opt.toAxisOption(p.theme))
}
-// NewRightYAxis returns a right y axis renderer
-func NewRightYAxis(p *Painter, opt YAxisOption) *axisPainter {
+// newRightYAxis returns a right y-axis renderer.
+func newRightYAxis(p *Painter, opt YAxisOption) *axisPainter {
p = p.Child(PainterPaddingOption(Box{
Bottom: defaultXAxisHeight,
}))
- axisOpt := opt.ToAxisOption(p)
+ axisOpt := opt.toAxisOption(p.theme)
axisOpt.Position = PositionRight
axisOpt.SplitLineShow = false
- return NewAxisPainter(p, axisOpt)
+ return newAxisPainter(p, axisOpt)
}
diff --git a/yaxis_test.go b/yaxis_test.go
index f01aca2..3d900e4 100644
--- a/yaxis_test.go
+++ b/yaxis_test.go
@@ -19,18 +19,18 @@ func TestRightYAxis(t *testing.T) {
opt := YAxisOption{
Data: []string{"a", "b", "c", "d"},
}
- _, err := NewRightYAxis(p, opt).Render()
+ _, err := newRightYAxis(p, opt).Render()
if err != nil {
return nil, err
}
return p.Bytes()
},
- result: "",
+ result: "",
},
}
for i, tt := range tests {
t.Run(strconv.Itoa(i), func(t *testing.T) {
- p, err := NewPainter(PainterOptions{
+ p := NewPainter(PainterOptions{
OutputFormat: ChartOutputSVG,
Width: 600,
Height: 400,
@@ -40,7 +40,6 @@ func TestRightYAxis(t *testing.T) {
Bottom: 10,
Left: 10,
}))
- require.NoError(t, err)
data, err := tt.render(p)
require.NoError(t, err)
assertEqualSVG(t, tt.result, data)