Files
ttychart-mcp/tools/charts/line.go
titor d3e04c5d1a feat: 初始版本 - ttychart-mcp 终端图表 MCP 服务
- 支持三种图表: 折线图、柱状图、散点图
- MCP 协议支持 (stdio + HTTP)
- 完整的单元测试和集成测试
- Docker 支持
- Makefile 构建脚本
2026-04-15 21:03:36 +08:00

489 lines
8.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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
}