feat: support line chart draw function

This commit is contained in:
vicanso 2022-01-29 11:16:34 +08:00
parent 4ac419fce9
commit ccdaf70dcb
34 changed files with 1780 additions and 4672 deletions

468
axis.go
View file

@ -1,6 +1,6 @@
// MIT License
// Copyright (c) 2021 Tree Xie
// 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
@ -23,176 +23,336 @@
package charts
import (
"math"
"github.com/dustin/go-humanize"
"github.com/golang/freetype/truetype"
"github.com/wcharczuk/go-chart/v2"
"github.com/wcharczuk/go-chart/v2/drawing"
)
type (
// AxisData string
XAxis struct {
// data value of axis
Data []string
// number of segments
SplitNumber int
}
)
type YAxisOption struct {
// formater of axis
Formater chart.ValueFormatter
// disabled axis
Disabled bool
// min value of axis
Min *float64
// max value of axis
Max *float64
type AxisStyle struct {
BoundaryGap *bool
Show *bool
Position string
ClassName string
StrokeColor drawing.Color
StrokeWidth float64
TickLength int
TickShow *bool
LabelMargin int
FontSize float64
Font *truetype.Font
FontColor drawing.Color
SplitLineShow bool
SplitLineColor drawing.Color
}
const axisStrokeWidth = 1
func (as *AxisStyle) GetLabelMargin() int {
return getDefaultInt(as.LabelMargin, 8)
}
// GetXAxisAndValues returns x axis by theme, and the values of axis.
func GetXAxisAndValues(xAxis XAxis, tickPosition chart.TickPosition, theme string) (chart.XAxis, []float64) {
data := xAxis.Data
originalSize := len(data)
// 如果居中,则需要多添加一个值
if tickPosition == chart.TickPositionBetweenTicks {
data = append([]string{
"",
}, data...)
func (as *AxisStyle) GetTickLength() int {
return getDefaultInt(as.TickLength, 5)
}
func (as *AxisStyle) Style(f *truetype.Font) chart.Style {
s := chart.Style{
ClassName: as.ClassName,
StrokeColor: as.StrokeColor,
StrokeWidth: as.StrokeWidth,
FontSize: as.FontSize,
FontColor: as.FontColor,
Font: as.Font,
}
size := len(data)
xValues := make([]float64, size)
ticks := make([]chart.Tick, 0)
// tick width
maxTicks := maxInt(xAxis.SplitNumber, 10)
// 计息最多每个unit至少放多个
minUnitSize := originalSize / maxTicks
if originalSize%maxTicks != 0 {
minUnitSize++
if s.FontSize == 0 {
s.FontSize = chart.DefaultFontSize
}
unitSize := minUnitSize
// 尽可能选择一格展示更多的块
for i := minUnitSize; i < 2*minUnitSize; i++ {
if originalSize%i == 0 {
unitSize = i
if s.Font == nil {
s.Font = f
}
return s
}
type AxisData struct {
Text string
}
type AxisDataList []AxisData
func (l AxisDataList) TextList() []string {
textList := make([]string, len(l))
for index, item := range l {
textList[index] = item.Text
}
return textList
}
type axisOption struct {
data *AxisDataList
style *AxisStyle
textMaxWith int
textMaxHeight int
boundaryGap bool
}
func NewAxisDataListFromStringList(textList []string) AxisDataList {
list := make(AxisDataList, len(textList))
for index, text := range textList {
list[index] = AxisData{
Text: text,
}
}
for index, key := range data {
f := float64(index)
xValues[index] = f
if index%unitSize == 0 || index == size-1 {
ticks = append(ticks, chart.Tick{
Value: f,
Label: key,
})
}
}
return chart.XAxis{
Ticks: ticks,
TickPosition: tickPosition,
Style: chart.Style{
FontColor: getAxisColor(theme),
StrokeColor: getAxisColor(theme),
StrokeWidth: axisStrokeWidth,
},
}, xValues
return list
}
func defaultFloatFormater(v interface{}) string {
value, ok := v.(float64)
if !ok {
return ""
}
// 大于10的则直接取整展示
if value >= 10 {
return humanize.CommafWithDigits(value, 0)
}
return humanize.CommafWithDigits(value, 2)
}
func newYContinuousRange(option *YAxisOption) *YContinuousRange {
m := YContinuousRange{}
m.Min = -math.MaxFloat64
m.Max = math.MaxFloat64
if option != nil {
if option.Min != nil {
m.Min = *option.Min
}
if option.Max != nil {
m.Max = *option.Max
}
}
return &m
}
// GetSecondaryYAxis returns the secondary y axis by theme
func GetSecondaryYAxis(theme string, option *YAxisOption) chart.YAxis {
strokeColor := getGridColor(theme)
yAxis := chart.YAxis{
Range: newYContinuousRange(option),
ValueFormatter: defaultFloatFormater,
AxisType: chart.YAxisSecondary,
GridMajorStyle: chart.Style{
StrokeColor: strokeColor,
StrokeWidth: axisStrokeWidth,
},
GridMinorStyle: chart.Style{
StrokeColor: strokeColor,
StrokeWidth: axisStrokeWidth,
},
Style: chart.Style{
FontColor: getAxisColor(theme),
// alpha 0隐藏
StrokeColor: hiddenColor,
StrokeWidth: axisStrokeWidth,
},
}
setYAxisOption(&yAxis, option)
return yAxis
}
func setYAxisOption(yAxis *chart.YAxis, option *YAxisOption) {
if option == nil {
func (d *Draw) axisLabel(opt *axisOption) {
style := opt.style
data := *opt.data
if style.FontColor.IsZero() || len(data) == 0 {
return
}
if option.Formater != nil {
yAxis.ValueFormatter = option.Formater
r := d.Render
s := style.Style(d.Font)
s.GetTextOptions().WriteTextOptionsToRenderer(r)
width := d.Box.Width()
height := d.Box.Height()
textList := data.TextList()
count := len(textList)
boundaryGap := opt.boundaryGap
if !boundaryGap {
count--
}
labelMargin := style.GetLabelMargin()
// 轴线
labelHeight := labelMargin + opt.textMaxHeight
labelWidth := labelMargin + opt.textMaxWith
// 坐标轴文本
position := style.Position
switch position {
case PositionLeft:
fallthrough
case PositionRight:
values := autoDivide(height, count)
textList := data.TextList()
// 由下往上
reverseIntSlice(values)
for index, text := range textList {
y := values[index] - 2
b := r.MeasureText(text)
if boundaryGap {
height := y - values[index+1]
y -= (height - b.Height()) >> 1
} else {
y += b.Height() >> 1
}
// 左右位置的x不一样
x := width - opt.textMaxWith
if position == PositionLeft {
x = labelWidth - b.Width() - 1
}
d.text(text, x, y)
}
default:
// 定位bottom重新计算y0的定位
y0 := height - labelMargin
if position == PositionTop {
y0 = labelHeight - labelMargin
}
values := autoDivide(width, count)
for index, text := range data.TextList() {
x := values[index]
leftOffset := 0
b := r.MeasureText(text)
if boundaryGap {
width := values[index+1] - x
leftOffset = (width - b.Width()) >> 1
} else {
// 左移文本长度
leftOffset = -b.Width() >> 1
}
d.text(text, x+leftOffset, y0)
}
}
}
// GetYAxis returns the primary y axis by theme
func GetYAxis(theme string, option *YAxisOption) chart.YAxis {
disabled := false
if option != nil {
disabled = option.Disabled
}
hidden := chart.Hidden()
func (d *Draw) axisLine(opt *axisOption) {
r := d.Render
style := opt.style
s := style.Style(d.Font)
s.GetStrokeOptions().WriteDrawingOptionsToRenderer(r)
yAxis := chart.YAxis{
Range: newYContinuousRange(option),
ValueFormatter: defaultFloatFormater,
AxisType: chart.YAxisPrimary,
GridMajorStyle: hidden,
GridMinorStyle: hidden,
Style: chart.Style{
FontColor: getAxisColor(theme),
// alpha 0隐藏
StrokeColor: hiddenColor,
StrokeWidth: axisStrokeWidth,
},
x0 := 0
y0 := 0
x1 := 0
y1 := 0
width := d.Box.Width()
height := d.Box.Height()
labelMargin := style.GetLabelMargin()
// 轴线
labelHeight := labelMargin + opt.textMaxHeight
labelWidth := labelMargin + opt.textMaxWith
tickLength := style.GetTickLength()
switch style.Position {
case PositionLeft:
x0 = tickLength + labelWidth
x1 = x0
y0 = 0
y1 = height
case PositionRight:
x0 = width - labelWidth
x1 = x0
y0 = 0
y1 = height
case PositionTop:
x0 = 0
x1 = width
y0 = labelHeight
y1 = y0
// bottom
default:
x0 = 0
x1 = width
y0 = height - tickLength - labelHeight
y1 = y0
}
// 如果禁用则默认为隐藏并设置range
if disabled {
yAxis.Range = &HiddenRange{}
yAxis.Style.Hidden = true
}
setYAxisOption(&yAxis, option)
return yAxis
d.moveTo(x0, y0)
d.lineTo(x1, y1)
r.FillStroke()
}
func (d *Draw) axisTick(opt *axisOption) {
r := d.Render
style := opt.style
s := style.Style(d.Font)
s.GetStrokeOptions().WriteDrawingOptionsToRenderer(r)
width := d.Box.Width()
height := d.Box.Height()
data := *opt.data
tickCount := len(data)
if !opt.boundaryGap {
tickCount--
}
labelMargin := style.GetLabelMargin()
tickLengthValue := style.GetTickLength()
labelHeight := labelMargin + opt.textMaxHeight
labelWidth := labelMargin + opt.textMaxWith
position := style.Position
switch position {
case PositionLeft:
fallthrough
case PositionRight:
values := autoDivide(height, tickCount)
// 左右仅是x0的位置不一样
x0 := width - labelWidth
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 style.SplitLineShow && !style.SplitLineColor.IsZero() {
r.SetStrokeColor(style.SplitLineColor)
splitLineWidth := width - labelWidth
if position == PositionRight {
x0 = 0
splitLineWidth = width - labelWidth - 1
}
for _, v := range values[0 : len(values)-1] {
x := x0
y := v
d.moveTo(x, y)
d.lineTo(x+splitLineWidth, y)
r.Stroke()
}
}
default:
values := autoDivide(width, tickCount)
// 上下仅是y0的位置不一样
y0 := height - labelHeight
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 style.SplitLineShow && !style.SplitLineColor.IsZero() {
r.SetStrokeColor(style.SplitLineColor)
y0 = 0
splitLineHeight := height - labelHeight - tickLengthValue
if position == PositionTop {
y0 = labelHeight
splitLineHeight = height - labelHeight
}
for _, v := range values {
x := v
y := y0
d.moveTo(x, y)
d.lineTo(x, y0+splitLineHeight)
r.Stroke()
}
}
}
}
func (d *Draw) axisMeasureTextMaxWidthHeight(data AxisDataList, style AxisStyle) (int, int) {
r := d.Render
s := style.Style(d.Font)
s.GetStrokeOptions().WriteDrawingOptionsToRenderer(r)
s.GetTextOptions().WriteTextOptionsToRenderer(r)
return measureTextMaxWidthHeight(data.TextList(), r)
}
// measureAxis return the measurement of axis.
// If the position is left or right, it will be textMaxWidth + labelMargin + tickLength.
// If the position is top or bottom, it will be textMaxHeight + labelMargin + tickLength.
func (d *Draw) measureAxis(data AxisDataList, style AxisStyle) int {
value := style.GetLabelMargin() + style.GetTickLength()
textMaxWidth, textMaxHeight := d.axisMeasureTextMaxWidthHeight(data, style)
if style.Position == PositionLeft ||
style.Position == PositionRight {
return textMaxWidth + value
}
return textMaxHeight + value
}
func (d *Draw) Axis(data AxisDataList, style AxisStyle) {
if style.Show != nil && !*style.Show {
return
}
textMaxWidth, textMaxHeight := d.axisMeasureTextMaxWidthHeight(data, style)
opt := &axisOption{
data: &data,
style: &style,
textMaxWith: textMaxWidth,
textMaxHeight: textMaxHeight,
boundaryGap: true,
}
if style.BoundaryGap != nil && !*style.BoundaryGap {
opt.boundaryGap = false
}
// 坐标轴线
d.axisLine(opt)
d.axisTick(opt)
// 坐标文本
d.axisLabel(opt)
}