feat: support line chart draw function
This commit is contained in:
parent
4ac419fce9
commit
ccdaf70dcb
34 changed files with 1780 additions and 4672 deletions
468
axis.go
468
axis.go
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue