// Package charts 图表实现包 // // 该包提供各种终端图表的纯 Go 实现 // 不依赖外部大型库,保持轻量级 // // 作者: titor // 创建日期: 2026-04-15 package charts import ( "fmt" "math" "charm.land/lipgloss/v2" ) // LineChart 折线图结构体 // // 用于生成 ASCII 折线图 type LineChart struct { // 图表宽度 Width int // 图表高度 Height int // Y轴数据 Data []float64 // 图表标题 Title string // X轴标签 Labels []string // 线条颜色 Color lipgloss.Style } // NewLineChart 创建新的折线图 // // 参数: // - width: 图表宽度(0 表示自动) // - height: 图表高度(0 表示自动) // // 返回值: // - *LineChart: 折线图实例 func NewLineChart(width, height int) *LineChart { return &LineChart{ Width: width, Height: height, } } // SetData 设置数据 func (c *LineChart) SetData(data []float64) { c.Data = data } // SetTitle 设置标题 func (c *LineChart) SetTitle(title string) { c.Title = title } // SetLabels 设置标签 func (c *LineChart) SetLabels(labels []string) { c.Labels = labels } // SetColor 设置颜色 func (c *LineChart) SetColor(style lipgloss.Style) { c.Color = style } // Render 渲染图表 // // 返回值: // - string: 渲染后的 ASCII 图表 func (c *LineChart) Render() string { if len(c.Data) == 0 { return "错误: 没有数据" } // 计算合适的宽高 width := c.Width height := c.Height if width == 0 { width = len(c.Data) * 3 if width < 30 { width = 30 } } if height == 0 { height = 10 } // 计算数据范围 minVal, maxVal := c.findMinMax() rangeVal := maxVal - minVal if rangeVal == 0 { rangeVal = 1 } // 生成图表 var result string // 添加标题 if c.Title != "" { result += c.Title + "\n" } // Y轴标签宽度 labelWidth := 6 if maxVal >= 1000 { labelWidth = 8 } // 渲染每一行 for row := height - 1; row >= 0; row-- { // 计算这一行对应的 value value := minVal + (float64(row)/float64(height-1))*rangeVal // 输出 Y轴标签 result += fmt.Sprintf("%*.*f ", -labelWidth, 1, value) // 输出数据点 for i := 0; i < len(c.Data) && i < width; i++ { pos := float64(i) / float64(width-1) * rangeVal dataVal := c.Data[i] - minVal // 计算字符 if pos <= dataVal && dataVal >= pos { if row == height-1 && c.Data[i] >= maxVal { result += "╭" } else if row == 0 && c.Data[i] <= minVal { result += "╰" } else { result += "│" } } else { result += " " } } result += "\n" } // 输出 X轴 if len(c.Data) > 0 { result += fmt.Sprintf("%*s", labelWidth, "") for i := 0; i < len(c.Data) && i < width; i++ { result += "─" } result += "\n" } // 输出标签 if len(c.Labels) > 0 { result += fmt.Sprintf("%*s", labelWidth, "") for i, label := range c.Labels { if i < width { result += label } } result += "\n" } return result } // findMinMax 查找数据的最小最大值 // // 返回值: // - min: 最小值 // - max: 最大值 func (c *LineChart) findMinMax() (float64, float64) { if len(c.Data) == 0 { return 0, 1 } minVal := c.Data[0] maxVal := c.Data[0] for _, v := range c.Data { if v < minVal { minVal = v } if v > maxVal { maxVal = v } } // 添加一些边距 margin := (maxVal - minVal) * 0.1 if margin == 0 { margin = 1 } return minVal - margin, maxVal + margin } // BarDataItem 柱状图数据项 type BarDataItem struct { // 标签 Label string // 数值 Value float64 } // BarChart 柱状图结构体 type BarChart struct { Width int Height int Horizontal bool Data []BarDataItem Title string Color lipgloss.Style } // NewBarChart 创建新的柱状图 func NewBarChart(width, height int, horizontal bool) *BarChart { return &BarChart{ Width: width, Height: height, Horizontal: horizontal, } } // SetData 设置数据 func (c *BarChart) SetData(data []BarDataItem) { c.Data = data } // SetTitle 设置标题 func (c *BarChart) SetTitle(title string) { c.Title = title } // SetColor 设置颜色 func (c *BarChart) SetColor(style lipgloss.Style) { c.Color = style } // Render 渲染柱状图 func (c *BarChart) Render() string { if len(c.Data) == 0 { return "错误: 没有数据" } // 计算最大值 maxVal := float64(0) for _, item := range c.Data { if item.Value > maxVal { maxVal = item.Value } } if maxVal == 0 { maxVal = 1 } width := c.Width if width == 0 { width = 60 } height := c.Height if height == 0 { height = len(c.Data) if height < 5 { height = 5 } } var result string // 标题 if c.Title != "" { result += c.Title + "\n" } // 渲染 barChar := "█" if c.Horizontal { // 水平柱状图 for i, item := range c.Data { if i >= height { break } barLen := int(item.Value / maxVal * float64(width-len(item.Label)-2)) if barLen < 1 { barLen = 1 } result += fmt.Sprintf("%-*s ", len(item.Label), item.Label) for j := 0; j < barLen; j++ { result += barChar } result += fmt.Sprintf(" %.2f\n", item.Value) } } else { // 垂直柱状图 labelWidth := 8 for row := height - 1; row >= 0; row-- { for i, item := range c.Data { if i >= width { break } barHeight := int(item.Value / maxVal * float64(height)) if row < barHeight { result += barChar } else { result += " " } } result += "\n" } // 标签 for _, item := range c.Data { label := item.Label if len(label) > labelWidth { label = label[:labelWidth] } result += fmt.Sprintf("%-*s ", labelWidth, label) } result += "\n" } return result } // ScatterDataItem 散点图数据项 type ScatterDataItem struct { X float64 Y float64 } // ScatterChart 散点图结构体 type ScatterChart struct { Width int Height int Data []ScatterDataItem Title string XLabel string YLabel string Color lipgloss.Style } // NewScatterChart 创建新的散点图 func NewScatterChart(width, height int) *ScatterChart { return &ScatterChart{ Width: width, Height: height, } } // SetData 设置数据 func (c *ScatterChart) SetData(data []ScatterDataItem) { c.Data = data } // SetTitle 设置标题 func (c *ScatterChart) SetTitle(title string) { c.Title = title } // SetXLabel 设置 X 轴标签 func (c *ScatterChart) SetXLabel(label string) { c.XLabel = label } // SetYLabel 设置 Y 轴标签 func (c *ScatterChart) SetYLabel(label string) { c.YLabel = label } // SetColor 设置颜色 func (c *ScatterChart) SetColor(style lipgloss.Style) { c.Color = style } // Render 渲染散点图 func (c *ScatterChart) Render() string { if len(c.Data) == 0 { return "错误: 没有数据" } width := c.Width height := c.Height if width == 0 { width = 50 } if height == 0 { height = 20 } // 找数据范围 minX, maxX := c.findMinMaxX() minY, maxY := c.findMinMaxY() // 添加边距 marginX := (maxX - minX) * 0.1 marginY := (maxY - minY) * 0.1 if marginX == 0 { marginX = 1 } if marginY == 0 { marginY = 1 } minX -= marginX maxX += marginX minY -= marginY maxY += marginY var result string // 标题 if c.Title != "" { result += c.Title + "\n" } // 创建点阵 grid := make([][]rune, height) for i := range grid { grid[i] = make([]rune, width) for j := range grid[i] { grid[i][j] = ' ' } } // 绘制数据点 for _, point := range c.Data { // 映射到网格 x := int((point.X - minX) / (maxX - minX) * float64(width-1)) y := int((point.Y - minY) / (maxY - minY) * float64(height-1)) y = height - 1 - y // 反转 Y 轴 // 边界检查 if x >= 0 && x < width && y >= 0 && y < height { grid[y][x] = '●' } } // 输出图表 for _, row := range grid { result += string(row) + "\n" } // X轴标签 if c.XLabel != "" { result += c.XLabel + "\n" } return result } // findMinMaxX 查找 X 数据的最小最大值 func (c *ScatterChart) findMinMaxX() (float64, float64) { if len(c.Data) == 0 { return 0, 1 } minVal := c.Data[0].X maxVal := c.Data[0].X for _, v := range c.Data { if v.X < minVal { minVal = v.X } if v.X > maxVal { maxVal = v.X } } if math.IsInf(minVal, 0) || math.IsInf(maxVal, 0) { return 0, 1 } return minVal, maxVal } // findMinMaxY 查找 Y 数据的最小最大值 func (c *ScatterChart) findMinMaxY() (float64, float64) { if len(c.Data) == 0 { return 0, 1 } minVal := c.Data[0].Y maxVal := c.Data[0].Y for _, v := range c.Data { if v.Y < minVal { minVal = v.Y } if v.Y > maxVal { maxVal = v.Y } } if math.IsInf(minVal, 0) || math.IsInf(maxVal, 0) { return 0, 1 } return minVal, maxVal }