外观
图表
ui/chart 包构建自身不带外观的图表,就像 Web 上 visx 这类无头(headless) 库那样,并借鉴了 Swift Charts 的组成部分:把数据映射到绘图区并挑选整齐刻度的 标度、四周带坐标轴的绘图区、可以像其他元素一样随意设置样式的标记(mark)、 供 Draw 函数使用的折线、区域与饼块路径、堆叠,以及可由指针和键盘移动的 选中项。每个颜色、尺寸和字体都由你决定。
go
import "github.com/egoist/mygo/ui/chart"
t := c.Theme()
x := chart.NewBand(app.months...).Padding(0.3, 0.15)
_, hi := chart.Extent(app.sales...)
y := chart.NewLinear(0, hi).Nice(4)
ticks := y.Ticks(4)
chart.Plot(c, chart.Axes{Left: ticks, Bottom: x.Ticks()}, func(area ui.Element) {
area.Draw(func(p *ui.Painter, r ui.Rect) {
for _, tick := range ticks { // grid lines
p.Line(r.X, chart.Y(r, tick.At), r.X+r.W, chart.Y(r, tick.At), 1, t.Border)
}
})
for i, v := range app.sales {
chart.Rect(c, x.Band(i), y.Span(0, v)).Radius(4).Background(t.Accent).
Tooltip(fmt.Sprintf("%s: %.0f", app.months[i], v))
}
}).Label("Sales by month").Height(200).Gap(6)绘图区上的位置是它的比例,从 0 到 1:沿 x 从左边缘算起,沿 y 从底部向上, 与数值增长的方向一致。标度把数据变成比例;标记按绘图区的百分比定位自己, 因此无需等待布局即可构建,并随绘图区调整大小而跟随;Draw 函数得到绘图区 的矩形,chart.X(r, f) 和 chart.Y(r, f) 把比例变成其中的点。
标度
chart.NewLinear(lo, hi)映射数值。Nice(n)把定义域向外取整到约 n 个 刻度的整齐值,Ticks(n)返回这些刻度:间隔为 1、2 或 5 乘十的幂,与 d3 和 Swift Charts 的选取方式一致,小数位数取决于步长所需,或用Format标注。At(v)给出某个数值所在的位置,Span(a, b)给出两个值之间的区段,比如 条形从 0 到其值,Invert(f)给出某个比例处的值,比如指针下方的值。Clamp把超出定义域的值保持在两端;chart.Extent(values...)返回最小值 和最大值。chart.NewBand(labels...)给每个标签一个条带,就像柱状图的柱子:Band(i)是第 i 个条带的区段,At(i)是它的中心,Width()是条带的宽度,Index(f)返回某个比例处的条带。Padding(inner, outer)在条带之间和两端留出空间, 以条带步长的比例计。它的Ticks()就是这些标签,位于各自条带的中心。chart.NewTime(from, to)映射时间。它的刻度落在时间所在时区的整齐时刻上, 按跨度选取,方式与 d3 相同:整秒、一刻钟、午夜、周日、每月的 1 号或每年的 1 月;每个刻度标注自上一粗一级整齐时刻以来变化的部分:"2026"、"Mar"、 "Mar 5"、"14:00"、":30"。
刻度是 chart.Tick 值,包含比例 At、Value 和 Label:对于标度未标注的 坐标轴,可以自己构造。
绘图区与坐标轴
chart.Plot 把绘图区的区域按网格布局,chart.Axes 给刻度的每一侧各有一条 坐标轴:Left 和 Right 沿高度方向,Top 和 Bottom 沿宽度方向。标签留在 布局中,因此坐标轴占据所需的空间,区域得到其余部分:给绘图区一个高度,或让 它 Fill 父元素,并设置标签与区域之间的 Gap(或 GapX 和 GapY)。 Axes.Label 构建每个标签,居中对齐它的刻度;默认是纯文本。标签放在宽高为 零、把它们居中的盒子里,所以要用 NoWrap 让文字保持在一行:
go
label := func(side chart.Side, tick chart.Tick) {
ui.Text(c, tick.Label).NoWrap().FontSize(11).TextColor(t.TextMuted)
}
chart.Plot(c, chart.Axes{Left: y.Ticks(4), Bottom: x.Ticks(), Label: label}, build)Plot 的函数得到区域,它是一个可以设置样式的元素:可以有背景、边框,或有 绘制网格线和折线的 Draw 函数,并可以向它添加标记。
标记
标记是区域内的元素,按区域的百分比定位:像任何元素一样为它们设置样式、 为辅助技术命名、添加工具提示并处理点击。
chart.Rect(c, x, y)覆盖沿 x 的一段区段并沿 y 向上延伸:一根柱子、 一个区间的条带、热力图的一个格子。chart.Point(c, x, y, w, h)是 w×h DIP、以某个点为中心的方块:散点图的点、 选中项所在的点。chart.RuleX(c, x, width)是贯穿绘图区的竖线,chart.RuleY(c, y, height)是横线:阈值、平均值。chart.Anchor(c, x, y, h, v, build)把build添加的内容(任意大小)放在 某个点上:ui.Center把它在那里居中,ui.Start把它的左边缘(h)或上边缘 (v)放在那里,ui.End放右边缘或下边缘,因此ui.Center, ui.End会把标签 显示在柱子上方。
go
for i, v := range app.sales {
bar := chart.Rect(c, x.Band(i), y.Span(0, v)).Background(t.Accent)
if bar.Clicked() {
app.month = i
}
chart.Anchor(c, x.At(i), y.At(v), ui.Center, ui.End, func() {
ui.Text(c, fmt.Sprint(v)).NoWrap().FontSize(11).Margin(0, 0, 4, 0)
})
}带 Transition 的标记会随数据变化移动到新位置,比如柱子 长到新数值:
go
chart.Rect(c, x.Band(i), y.Span(0, v)).Background(t.Accent).
Transition(ui.ElementTransition{Duration: 300 * time.Millisecond})折线与区域
chart.Line(r, xs, ys, curve) 返回穿过各点的折线路径,chart.Area(r, xs, lo, hi, curve) 返回两条线之间区域的路径,供区域的 Draw 函数描边和填充。 xs 和 ys 是比例,即标度返回的值;lo 为 nil 表示绘图区底部,NaN 会在 此处断开折线或区域,用于缺失值。曲线连接各点的方式:chart.CurveLinear 用 直线,chart.CurveMonotone 平滑连接且不超过所连各点的数值, chart.CurveStepAfter、CurveStepBefore 和 CurveStep 呈阶梯状。
go
area.Draw(func(p *ui.Painter, r ui.Rect) {
p.FillPath(chart.Area(r, xs, nil, ys, chart.CurveMonotone), t.Accent.Alpha(0.15))
p.StrokePath(chart.Line(r, xs, ys, chart.CurveMonotone), 2, t.Accent)
})实时数据
上千个采样点的折线,随着新采样到达而每帧重绘,需要的点数比采样数少: chart.MinMax 为每个像素列保留最低和最高的采样,保持它们的顺序,并保留 每段的端点,使折线看起来相同,而窄的尖峰仍然可见。它的 xs 是不会递减的 比例;NaN 仍会在此处断开折线。它写入你传入的切片,chart.LineInto 写入你 传入的路径,因此持有它们的视图在预热之后绘制时不再分配内存。 采样本身要保留下来,供选中和读数使用。
Painter.StrokePolyline 以 DIP 为单位描点,使用圆角连接和端点,效果与 StrokePath 相同,但对每帧都在变化的线更快:在 GPU 上,它把每段绘制成 圆角矩形,而不是在 CPU 上把整条线栅格化成遮罩。线段重叠处,抗锯齿边缘可能 看起来比 StrokePath 的略粗。半透明的线,以及不用 GPU 的绘制,走 StrokePath 的方式,因为重叠的线段在那里会显得更暗。
go
// bx, by and points live in the view's state, reused every frame.
area.Draw(func(p *ui.Painter, r ui.Rect) {
bx, by = chart.MinMax(bx, by, xs, ys, int(math.Ceil(float64(r.W*p.Scale()))))
points = points[:0]
for i, x := range bx {
points = append(points, [2]float32{chart.X(r, x), chart.Y(r, by[i])})
}
p.StrokePolyline(points, 2, t.Accent)
})用 Context.AnimationFrame 请求帧,并接收自上一帧以来到达的采样,而不是 每个采样都使视图失效。examples/chart-streaming 滚动三条各 15,000 个采样 的波形,每秒各进 1,000 个;-mode path 和 -mode raw 用 StrokePath 绘制它们,以便对比。
堆叠
chart.Stack(series, how) 把多个序列堆叠起来,供堆叠柱状图和堆叠区域图 展示:series[s][i] 是第 s 个序列的第 i 个值,结果的 [s][i] 是它在第 i 个 堆叠中覆盖的 Segment。chart.StackStandard 从 0 向上堆叠正值、向下堆叠 负值;chart.StackNormalized 按占堆叠总和的比例,从 0 到 1;chart.StackCenter 围绕 0 居中堆叠,如河流图(streamgraph)。
go
stacked := chart.Stack([][]float64{app.search, app.direct}, chart.StackStandard)
for s, series := range stacked {
for i, seg := range series {
chart.Rect(c, x.Band(i), y.Span(seg.Lo, seg.Hi)).Background(colors[s])
}
}饼图与环形图
chart.Pie(values, pad) 把一个圆从十二点钟方向起、按值的顺序分配给各值, 彼此间留 pad 弧度;chart.Sector(cx, cy, inner, outer, slice) 返回一块 扇形的路径:inner 为 0 时是楔形,否则是圆环的一段。chart.SliceAt 找到 某个点所在的扇形,比如指针下方;chart.Polar 找到某个角度上的点,即扇形 标签所在的位置:
go
slices := chart.Pie(app.shares, 0.03)
ring := ui.Box(c).Size(160, 160).Label("Users by platform")
hover := -1
if x, y, over := ring.PointerPosition(); over {
if i, ok := chart.SliceAt(slices, x-80, y-80, 50, 80); ok {
hover = i
}
}
ring.Draw(func(p *ui.Painter, r ui.Rect) {
for i, s := range slices {
outer := float32(76)
if i == hover {
outer = 80
}
p.FillPath(chart.Sector(r.X+80, r.Y+80, 50, outer, s), colors[i])
}
})选中
chart.Select 让区域从 xs 处按升序排列的各点中选出一个,类似 Swift Charts 的 chartXSelection:指针在区域上方时选最近的一个,指针离开后为无 (-1)。区域获得键盘焦点,作为 Tab 的一个停靠点;左右方向键、Home 和 End 在 有焦点时移动选择,Escape 清除选择。它会报告新的选择。用标记显示所选:
go
describe := func(i int) string { return fmt.Sprintf("%s: %.1f °C", hours[i], temps[i]) }
chart.Select(c, area, &app.selected, xs, describe)
if i := app.selected; i >= 0 {
chart.RuleX(c, xs[i], 1).Background(t.TextMuted)
chart.Point(c, xs[i], ys[i], 10, 10).Radius(5).Background(t.Accent)
chart.Anchor(c, xs[i], 1, ui.Center, ui.Start, func() {
ui.Text(c, describe(i)).NoWrap().Padding(3, 8).Radius(6).Background(t.Surface)
})
}对于条带标度的各点,xs 是它们的中心;chart.Nearest 找到离某个比例最近 的点,供你自己实现选中;条带的 Index 则找到所在的条带。
无障碍
用 Label 为绘图区命名,用 Description 说明它展示的内容。标记是元素: 为每个需要辅助技术朗读的标记命名,比如柱子的月份和数值。Select 会把所选 点告知辅助技术:describe 的返回值就是区域的值,屏幕阅读器会在方向键移到 某个点时朗读它。
本包未包含的内容:对数标度、缩放与平移、图例(如上,用你自己的行实现), 以及自动避让的标签。