feat: support title render function

This commit is contained in:
vicanso 2022-01-29 15:02:01 +08:00
parent ccdaf70dcb
commit ffbda8f214
12 changed files with 455 additions and 141 deletions

35
axis.go
View file

@ -239,6 +239,10 @@ func (d *Draw) axisTick(opt *axisOption) {
tickCount--
}
labelMargin := style.GetLabelMargin()
tickShow := true
if opt.style.TickShow != nil && !*opt.style.TickShow {
tickShow = false
}
tickLengthValue := style.GetTickLength()
labelHeight := labelMargin + opt.textMaxHeight
@ -254,17 +258,20 @@ func (d *Draw) axisTick(opt *axisOption) {
if style.Position == PositionLeft {
x0 = labelWidth
}
for _, v := range values {
x := x0
y := v
d.moveTo(x, y)
d.lineTo(x+tickLengthValue, y)
r.Stroke()
if tickShow {
for _, v := range values {
x := x0
y := v
d.moveTo(x, y)
d.lineTo(x+tickLengthValue, y)
r.Stroke()
}
}
// 辅助线
if style.SplitLineShow && !style.SplitLineColor.IsZero() {
r.SetStrokeColor(style.SplitLineColor)
splitLineWidth := width - labelWidth
splitLineWidth := width - labelWidth - tickLengthValue
x0 = labelWidth + tickLengthValue
if position == PositionRight {
x0 = 0
splitLineWidth = width - labelWidth - 1
@ -284,12 +291,14 @@ func (d *Draw) axisTick(opt *axisOption) {
if position == PositionTop {
y0 = labelHeight
}
for _, v := range values {
x := v
y := y0
d.moveTo(x, y-tickLengthValue)
d.lineTo(x, y)
r.Stroke()
if tickShow {
for _, v := range values {
x := v
y := y0
d.moveTo(x, y-tickLengthValue)
d.lineTo(x, y)
r.Stroke()
}
}
// 辅助线
if style.SplitLineShow && !style.SplitLineColor.IsZero() {

View file

@ -140,7 +140,7 @@ func TestAxis(t *testing.T) {
opt.style.BoundaryGap = FalseFlag()
return opt
},
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 44 5\nL 44 295\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 5\nL 44 5\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 54\nL 44 54\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 103\nL 44 103\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 151\nL 44 151\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 199\nL 44 199\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 247\nL 44 247\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 295\nL 44 295\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 5\nL 395 5\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 39 54\nL 395 54\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 39 103\nL 395 103\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 39 151\nL 395 151\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 39 199\nL 395 199\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 39 247\nL 395 247\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><text x=\"12\" y=\"299\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Mon</text><text x=\"16\" y=\"251\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Tue</text><text x=\"12\" y=\"203\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Wed</text><text x=\"16\" y=\"155\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Thu</text><text x=\"23\" y=\"107\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Fri</text><text x=\"19\" y=\"58\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Sat</text><text x=\"16\" y=\"9\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Sun</text></svg>",
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 44 5\nL 44 295\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 5\nL 44 5\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 54\nL 44 54\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 103\nL 44 103\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 151\nL 44 151\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 199\nL 44 199\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 247\nL 44 247\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 295\nL 44 295\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 44 5\nL 395 5\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 44 54\nL 395 54\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 44 103\nL 395 103\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 44 151\nL 395 151\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 44 199\nL 395 199\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 44 247\nL 395 247\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><text x=\"12\" y=\"299\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Mon</text><text x=\"16\" y=\"251\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Tue</text><text x=\"12\" y=\"203\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Wed</text><text x=\"16\" y=\"155\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Thu</text><text x=\"23\" y=\"107\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Fri</text><text x=\"19\" y=\"58\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Sat</text><text x=\"16\" y=\"9\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Sun</text></svg>",
},
// 文本居中展示
// axis位于left
@ -150,7 +150,7 @@ func TestAxis(t *testing.T) {
opt.style.Position = PositionLeft
return opt
},
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 44 5\nL 44 295\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 5\nL 44 5\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 47\nL 44 47\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 89\nL 44 89\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 131\nL 44 131\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 172\nL 44 172\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 213\nL 44 213\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 254\nL 44 254\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 295\nL 44 295\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 5\nL 395 5\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 39 47\nL 395 47\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 39 89\nL 395 89\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 39 131\nL 395 131\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 39 172\nL 395 172\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 39 213\nL 395 213\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 39 254\nL 395 254\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><text x=\"12\" y=\"280\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Mon</text><text x=\"16\" y=\"239\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Tue</text><text x=\"12\" y=\"198\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Wed</text><text x=\"16\" y=\"157\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Thu</text><text x=\"23\" y=\"115\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Fri</text><text x=\"19\" y=\"73\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Sat</text><text x=\"16\" y=\"31\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Sun</text></svg>",
result: "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"400\" height=\"300\">\\n<path d=\"M 44 5\nL 44 295\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 5\nL 44 5\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 47\nL 44 47\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 89\nL 44 89\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 131\nL 44 131\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 172\nL 44 172\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 213\nL 44 213\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 254\nL 44 254\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 39 295\nL 44 295\" style=\"stroke-width:1;stroke:rgba(0,0,0,1.0);fill:none\"/><path d=\"M 44 5\nL 395 5\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 44 47\nL 395 47\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 44 89\nL 395 89\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 44 131\nL 395 131\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 44 172\nL 395 172\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 44 213\nL 395 213\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><path d=\"M 44 254\nL 395 254\" style=\"stroke-width:1;stroke:rgba(0,0,0,0.2);fill:none\"/><text x=\"12\" y=\"280\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Mon</text><text x=\"16\" y=\"239\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Tue</text><text x=\"12\" y=\"198\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Wed</text><text x=\"16\" y=\"157\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Thu</text><text x=\"23\" y=\"115\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Fri</text><text x=\"19\" y=\"73\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Sat</text><text x=\"16\" y=\"31\" style=\"stroke-width:0;stroke:none;fill:rgba(0,0,0,1.0);font-size:12.8px;font-family:'Roboto Medium',sans-serif\">Sun</text></svg>",
},
// 文本按起始位置展示
// axis位于right

View file

@ -23,17 +23,13 @@
package charts
import (
"math"
"github.com/dustin/go-humanize"
"github.com/wcharczuk/go-chart/v2"
"github.com/wcharczuk/go-chart/v2/drawing"
)
type XAxisOption struct {
BoundaryGap *bool
Data []string
// TODO split number
}
type SeriesData struct {
Value float64
Style chart.Style
@ -43,31 +39,6 @@ type Point struct {
Y int
}
type Range struct {
originalMin float64
originalMax float64
divideCount int
Min float64
Max float64
Size int
Boundary bool
}
func (r *Range) getHeight(value float64) int {
v := 1 - value/(r.Max-r.Min)
return int(v * float64(r.Size))
}
func (r *Range) getWidth(value float64) int {
v := value / (r.Max - r.Min)
// 移至居中
if r.Boundary &&
r.divideCount != 0 {
v += 1 / float64(r.divideCount*2)
}
return int(v * float64(r.Size))
}
type Series struct {
Type string
Name string
@ -78,6 +49,7 @@ type Series struct {
type ChartOption struct {
Theme string
Title TitleOption
XAxis XAxisOption
Width int
Height int
@ -87,6 +59,29 @@ type ChartOption struct {
BackgroundColor drawing.Color
}
func (o *ChartOption) FillDefault(t *Theme) {
if o.BackgroundColor.IsZero() {
o.BackgroundColor = t.GetBackgroundColor()
}
if o.Title.Style.FontColor.IsZero() {
o.Title.Style.FontColor = t.GetTitleColor()
}
if o.Title.Style.FontSize == 0 {
o.Title.Style.FontSize = 14
}
if o.Title.Style.Font == nil {
o.Title.Style.Font, _ = chart.GetDefaultFont()
}
if o.Title.Style.Padding.IsZero() {
o.Title.Style.Padding = chart.Box{
Left: 5,
Top: 5,
Right: 5,
Bottom: 5,
}
}
}
func (o *ChartOption) getWidth() int {
if o.Width == 0 {
return 600
@ -102,8 +97,8 @@ func (o *ChartOption) getHeight() int {
}
func (o *ChartOption) getYRange(axisIndex int) Range {
min := float64(0)
max := float64(0)
min := math.MaxFloat64
max := -math.MaxFloat64
for _, series := range o.SeriesList {
if series.YAxisIndex != axisIndex {
@ -118,18 +113,8 @@ func (o *ChartOption) getYRange(axisIndex int) Range {
}
}
}
// TODO 对于小数的处理
divideCount := 6
r := Range{
originalMin: min,
originalMax: max,
Min: float64(int(min * 0.8)),
Max: max * 1.2,
divideCount: divideCount,
}
value := int((r.Max - r.Min) / float64(divideCount))
r.Max = float64(int(float64(value*divideCount) + r.Min))
// y轴分设置默认划分为6块
r := NewRange(min*0.9, max*1.1, 6)
return r
}

View file

@ -34,6 +34,7 @@ import (
const (
PositionLeft = "left"
PositionRight = "right"
PositionCenter = "center"
PositionTop = "top"
PositionBottom = "bottom"
)

View file

@ -31,78 +31,6 @@ type LineChartOption struct {
ChartOption
}
const YAxisWidth = 50
func drawXAxis(d *Draw, opt *XAxisOption, theme *Theme) (int, *Range, error) {
dXAxis, err := NewDraw(
DrawOption{
Parent: d,
},
PaddingOption(chart.Box{
Left: YAxisWidth,
}),
)
if err != nil {
return 0, nil, err
}
data := NewAxisDataListFromStringList(opt.Data)
style := AxisStyle{
BoundaryGap: opt.BoundaryGap,
StrokeColor: theme.GetAxisStrokeColor(),
FontColor: theme.GetAxisStrokeColor(),
StrokeWidth: 1,
}
boundary := true
max := float64(len(opt.Data))
if opt.BoundaryGap != nil && !*opt.BoundaryGap {
boundary = false
max--
}
dXAxis.Axis(data, style)
return d.measureAxis(data, style), &Range{
divideCount: len(opt.Data),
Min: 0,
Max: max,
Size: dXAxis.Box.Width(),
Boundary: boundary,
}, nil
}
func drawYAxis(d *Draw, opt *ChartOption, theme *Theme, xAxisHeight int) (*Range, error) {
yRange := opt.getYRange(0)
data := NewAxisDataListFromStringList(yRange.Values())
style := AxisStyle{
Position: PositionLeft,
BoundaryGap: FalseFlag(),
// StrokeColor: theme.GetAxisStrokeColor(),
FontColor: theme.GetAxisStrokeColor(),
StrokeWidth: 1,
SplitLineColor: theme.GetAxisSplitLineColor(),
SplitLineShow: true,
}
width := d.measureAxis(data, style)
dYAxis, err := NewDraw(
DrawOption{
Parent: d,
Width: d.Box.Width(),
// 减去x轴的高
Height: d.Box.Height() - xAxisHeight,
},
PaddingOption(chart.Box{
Left: YAxisWidth - width,
}),
)
if err != nil {
return nil, err
}
dYAxis.Axis(data, style)
yRange.Size = dYAxis.Box.Height()
return &yRange, nil
}
func NewLineChart(opt LineChartOption) (*Draw, error) {
d, err := NewDraw(
DrawOption{
@ -119,27 +47,35 @@ func NewLineChart(opt LineChartOption) (*Draw, error) {
theme := Theme{
mode: opt.Theme,
}
// 设置背景色
bg := opt.BackgroundColor
if bg.IsZero() {
bg = theme.GetBackgroundColor()
}
opt.FillDefault(&theme)
if opt.Parent == nil {
d.setBackground(opt.getWidth(), opt.getHeight(), bg)
d.setBackground(opt.getWidth(), opt.getHeight(), opt.BackgroundColor)
}
// 标题
_, titleHeight, err := drawTitle(d, &opt.Title)
if err != nil {
return nil, err
}
// xAxis
xAxisHeight, xRange, err := drawXAxis(d, &opt.XAxis, &theme)
if err != nil {
return nil, err
}
// 暂时仅支持单一yaxis
yRange, err := drawYAxis(d, &opt.ChartOption, &theme, xAxisHeight)
yRange, err := drawYAxis(d, &opt.ChartOption, &theme, xAxisHeight, chart.Box{
Top: titleHeight,
})
if err != nil {
return nil, err
}
sd, err := NewDraw(DrawOption{
Parent: d,
}, PaddingOption(chart.Box{
Top: titleHeight,
Left: YAxisWidth,
}))
if err != nil {
@ -166,9 +102,7 @@ func NewLineChart(opt LineChartOption) (*Draw, error) {
DotFillColor: dotFillColor,
})
}
}
// fmt.Println(yRange)
return d, nil
}

75
range.go Normal file
View file

@ -0,0 +1,75 @@
// MIT License
// Copyright (c) 2022 Tree Xie
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package charts
import (
"math"
)
type Range struct {
divideCount int
Min float64
Max float64
Size int
Boundary bool
}
func NewRange(min, max float64, divideCount int) Range {
r := math.Abs(max - min)
// 最小单位计算
unit := 5
if r > 100 {
unit = 20
}
unit = int((r/float64(divideCount))/float64(unit))*unit + unit
if min != 0 {
min = float64(int(min/float64(unit)) * unit)
// 如果是小于0int的时候向上取整了因此调整
if min < 0 {
min -= float64(unit)
}
}
max = min + float64(unit*divideCount)
return Range{
Min: min,
Max: max,
divideCount: divideCount,
}
}
func (r *Range) getHeight(value float64) int {
v := 1 - (value-r.Min)/(r.Max-r.Min)
return int(v * float64(r.Size))
}
func (r *Range) getWidth(value float64) int {
v := value / (r.Max - r.Min)
// 移至居中
if r.Boundary &&
r.divideCount != 0 {
v += 1 / float64(r.divideCount*2)
}
return int(v * float64(r.Size))
}

49
range_test.go Normal file
View file

@ -0,0 +1,49 @@
// MIT License
// Copyright (c) 2022 Tree Xie
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package charts
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestRange(t *testing.T) {
assert := assert.New(t)
r := NewRange(0, 8, 6)
assert.Equal(0.0, r.Min)
assert.Equal(30.0, r.Max)
r = NewRange(0, 12, 6)
assert.Equal(0.0, r.Min)
assert.Equal(30.0, r.Max)
r = NewRange(-13, 18, 6)
assert.Equal(-20.0, r.Min)
assert.Equal(40.0, r.Max)
r = NewRange(0, 400, 6)
assert.Equal(0.0, r.Min)
assert.Equal(480.0, r.Max)
}

View file

@ -120,3 +120,20 @@ func (t *Theme) GetBackgroundColor() drawing.Color {
}
return drawing.ColorWhite
}
func (t *Theme) GetTitleColor() drawing.Color {
if t.IsDark() {
return drawing.Color{
R: 238,
G: 241,
B: 250,
A: 255,
}
}
return drawing.Color{
R: 70,
G: 70,
B: 70,
A: 255,
}
}

112
title.go Normal file
View file

@ -0,0 +1,112 @@
// MIT License
// Copyright (c) 2022 Tree Xie
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package charts
import (
"strconv"
"strings"
"github.com/wcharczuk/go-chart/v2"
)
type TitleOption struct {
Text string
Style chart.Style
Left string
Top string
}
type titleMeasureOption struct {
width int
height int
text string
}
func drawTitle(d *Draw, opt *TitleOption) (int, int, error) {
if len(opt.Text) == 0 {
return 0, 0, nil
}
padding := opt.Style.Padding
titleDraw, err := NewDraw(DrawOption{
Parent: d,
}, PaddingOption(padding))
if err != nil {
return 0, 0, err
}
r := titleDraw.Render
opt.Style.GetTextOptions().WriteToRenderer(r)
arr := strings.Split(opt.Text, "\n")
textMaxWidth := 0
textMaxHeight := 0
width := 0
measureOptions := make([]titleMeasureOption, len(arr))
for index, str := range arr {
textBox := r.MeasureText(str)
w := textBox.Width()
h := textBox.Height()
if w > textMaxWidth {
textMaxWidth = w
}
if h > textMaxHeight {
textMaxHeight = h
}
measureOptions[index] = titleMeasureOption{
text: str,
width: w,
height: h,
}
}
width = textMaxWidth
titleX := 0
b := titleDraw.Box
switch opt.Left {
case PositionRight:
titleX = b.Width() - textMaxWidth
case PositionCenter:
titleX = b.Width()>>1 - (textMaxWidth >> 1)
default:
if strings.HasSuffix(opt.Left, "%") {
value, _ := strconv.Atoi(strings.ReplaceAll(opt.Left, "%", ""))
titleX = b.Width() * value / 100
} else {
value, _ := strconv.Atoi(opt.Left)
titleX = value
}
}
titleY := 0
// TODO TOP 暂只支持数值
if opt.Top != "" {
value, _ := strconv.Atoi(opt.Top)
titleY += value
}
for _, item := range measureOptions {
x := titleX + (textMaxWidth-item.width)>>1
titleDraw.text(item.text, x, titleY)
titleY += textMaxHeight
}
height := titleY + padding.Top + padding.Bottom
return width, height, nil
}

View file

@ -67,6 +67,7 @@ func maxInt(values ...int) int {
return result
}
// measureTextMaxWidthHeight returns maxWidth and maxHeight of text list
func measureTextMaxWidthHeight(textList []string, r chart.Renderer) (int, int) {
maxWidth := 0
maxHeight := 0

69
xaxis.go Normal file
View file

@ -0,0 +1,69 @@
// MIT License
// Copyright (c) 2022 Tree Xie
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package charts
import "github.com/wcharczuk/go-chart/v2"
type XAxisOption struct {
BoundaryGap *bool
Data []string
// TODO split number
}
// drawXAxis draws x axis, and returns the height, range of if.
func drawXAxis(d *Draw, opt *XAxisOption, theme *Theme) (int, *Range, error) {
dXAxis, err := NewDraw(
DrawOption{
Parent: d,
},
PaddingOption(chart.Box{
Left: YAxisWidth,
}),
)
if err != nil {
return 0, nil, err
}
data := NewAxisDataListFromStringList(opt.Data)
style := AxisStyle{
BoundaryGap: opt.BoundaryGap,
StrokeColor: theme.GetAxisStrokeColor(),
FontColor: theme.GetAxisStrokeColor(),
StrokeWidth: 1,
}
boundary := true
max := float64(len(opt.Data))
if opt.BoundaryGap != nil && !*opt.BoundaryGap {
boundary = false
max--
}
dXAxis.Axis(data, style)
return d.measureAxis(data, style), &Range{
divideCount: len(opt.Data),
Min: 0,
Max: max,
Size: dXAxis.Box.Width(),
Boundary: boundary,
}, nil
}

62
yaxis.go Normal file
View file

@ -0,0 +1,62 @@
// MIT License
// Copyright (c) 2022 Tree Xie
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package charts
import (
"github.com/wcharczuk/go-chart/v2"
)
const YAxisWidth = 40
func drawYAxis(d *Draw, opt *ChartOption, theme *Theme, xAxisHeight int, padding chart.Box) (*Range, error) {
yRange := opt.getYRange(0)
data := NewAxisDataListFromStringList(yRange.Values())
style := AxisStyle{
Position: PositionLeft,
BoundaryGap: FalseFlag(),
FontColor: theme.GetAxisStrokeColor(),
TickShow: FalseFlag(),
StrokeWidth: 1,
SplitLineColor: theme.GetAxisSplitLineColor(),
SplitLineShow: true,
}
width := d.measureAxis(data, style)
padding.Left += (YAxisWidth - width)
dYAxis, err := NewDraw(
DrawOption{
Parent: d,
Width: d.Box.Width(),
// 减去x轴的高
Height: d.Box.Height() - xAxisHeight,
},
PaddingOption(padding),
)
if err != nil {
return nil, err
}
dYAxis.Axis(data, style)
yRange.Size = dYAxis.Box.Height()
return &yRange, nil
}