图表与画布
Chart、Canvas、DrawingContext 和 Path 的使用模式。
Chart 提供系统原生图表视图。简单图表继续用 x / y / type;组合图、范围图、阈值、逐层样式和扇区图使用 marks=[BarMark(...), LineMark(...), ...]。坐标、插值、图例、滚动、触摸选择和动画在原生端执行,Python 只处理数据与离散选择结果。Canvas 用绘制命令渲染自定义图形,可用 DrawingContext 链式生成命令,或直接传入 commands 字典列表。Path 则用于矢量路径(move / line / close 等)。
#预期效果
示例会展示柱状/折线图、Canvas 绘制和 Path 图形在页面中的结果。
#概念速览
| 能力 | API |
|---|---|
| 统计图 | Chart(data, x, y, type='bar', color=None, series=None, ...) |
| 原生 Mark 组合 | Chart(data, marks=[BarMark(...), LineMark(...)], selection_x=...) |
| 类型化字段/常量 | ChartField(key, label, value_type) / ChartValue(value, label, value_type) |
| 连续数值 / 日期轴 | x_type='number' / 'date' |
| 原生滚动与触摸选择 | scrollable_axes、visible_domain、selection、on_select |
| 画布绘制 | DrawingContext().fill_rect(...)... 与 Canvas(width, height, context=ctx) |
| 原始命令 | Canvas(width, height, commands=[{'op': 'fill_rect', ...}, ...]) |
| 矢量路径 | Path(commands=[...], fill=..., stroke=..., line_width=...) |
#旧写法的五种 Chart 类型
bar:离散类别对比,适合少量枚举横轴(如月份、地区)。line:连续或有序横轴上的趋势;与series组合可绘制多条线。area:与折线类似,但填充下方区域,强调「累积感」或占比。point:强调散点分布,适合样本量不大时的离群观察。rule:水平或垂直参考线,适合阈值、平均线等「标线」场景(数据需自行构造为常数y或x)。
#生产级参数
interpolation:linear、monotone、cardinal、catmull_rom、step_start、step_center、step_end。stacking:standard、normalized、center、unstacked。symbol/symbol_size:折线与散点的系统符号。show_legend、show_x_axis、show_y_axis、y_domain:图例、坐标轴和固定数值域。selection=state.bind.x:触摸选择与 Python 状态双向同步;on_select(row)得到最接近的原始数据字典。scrollable_axes='horizontal'+visible_domain=...:大于屏幕宽度的数据使用系统图表滚动。annotation_key:把某个字段作为原生 mark 标注。marks:1–32 个 Mark 图层;可用BarMark、LineMark、AreaMark、PointMark、RuleMark、RectangleMark、SectorMark。selection_x:组合 Mark 用于触摸选择和最近数据行匹配的字段;省略时沿用x。
#什么时候使用组合 Mark
一种图形、统一颜色、统一插值时,旧的 type= 写法更短,也会长期兼容。以下情况用 marks=:
- 同一图中叠加面积、折线、散点与阈值。
- 需要区间柱、范围面积或
RectangleMark。 - 每个图层需要独立颜色、透明度、线宽、虚线、symbol 或标注。
- 需要 iOS 17+ 的
SectorMark饼图/环形图。
Mark 坐标中的字符串表示数据字段。要表达常量,尤其是常量字符串,使用 ChartValue;需要明确轴标题或字段类型时使用 ChartField。
import appui
ROWS = [
{"day": "Mon", "value": 6},
{"day": "Tue", "value": 9},
{"day": "Wed", "value": 8},
{"day": "Thu", "value": 13},
]
def body():
x = appui.ChartField("day", label="日期", value_type="category")
y = appui.ChartField("value", label="数值", value_type="number")
return appui.Chart(
ROWS,
marks=[
appui.AreaMark(x, y, interpolation="monotone", opacity=0.15),
appui.LineMark(x, y, interpolation="monotone", line_width=3),
appui.PointMark(x, y, symbol="circle", symbol_size=30),
appui.RuleMark(
y=appui.ChartValue(10, label="目标"),
dash=[5, 3],
foreground_style="systemOrange",
),
],
selection_x="day",
).frame(height=240)
appui.run(body)
#Canvas 两条路径
- 声明式:
DrawingContext链式调用,最后交给Canvas(..., context=ctx);可读性高,适合中等数量图元。 - 命令列表:直接维护
commands列表,适合波形、频谱等高频追加场景。
#基础示例
同一组销售数据,切换 Chart 的 type。
import appui
state = appui.State(chart_kind="bar")
SALES = [
{"m": "1月", "v": 12},
{"m": "2月", "v": 19},
{"m": "3月", "v": 15},
{"m": "4月", "v": 22},
]
def set_chart_kind(value):
state.chart_kind = value
def body():
return appui.NavigationStack(
appui.VStack(
[
appui.Picker(
label="图表类型",
selection=state.chart_kind,
options=["bar", "line", "area", "point", "rule"],
on_change=set_chart_kind,
).picker_style("segmented"),
appui.Chart(
data=SALES,
x="m",
y="v",
type=state.chart_kind,
color="systemBlue",
).frame(height=220),
],
spacing=16,
)
.padding()
.navigation_title("Chart 类型")
)
appui.run(body, state=state)
#进阶示例
多系列折线(series 键)、完整 DrawingContext 演示、Canvas 原始 commands、Path 三角形。
import appui
state = appui.State()
MULTI = [
{"day": "周一", "amount": 3, "team": "A"},
{"day": "周二", "amount": 5, "team": "A"},
{"day": "周一", "amount": 4, "team": "B"},
{"day": "周二", "amount": 2, "team": "B"},
]
def body():
ctx = appui.DrawingContext()
ctx.fill_rect(0, 0, 300, 120, color="secondarySystemBackground")
ctx.stroke_rect(8, 8, 100, 60, color="label", line_width=1)
ctx.fill_circle(150, 40, 22, color="systemRed")
ctx.stroke_circle(150, 40, 28, color="systemOrange", line_width=2)
ctx.fill_ellipse(190, 15, 50, 50, color="systemPurple")
ctx.stroke_ellipse(190, 15, 50, 50, color="systemYellow", line_width=2)
ctx.line(10, 100, 290, 100, color="separator", line_width=1)
ctx.fill_text("DrawingContext", 12, 88, color="label", font_size=14)
ctx.fill_path([(200, 75), (260, 110), (170, 110)], color="systemGreen", close=True)
ctx.stroke_path([(20, 70), (60, 95), (40, 50)], color="systemBlue", line_width=2, close=True)
ctx.arc(80, 95, 18, start_angle=0, end_angle=270, color="systemTeal", line_width=2, fill=False)
ctx.rounded_rect(
230, 70, 60, 40,
corner_radius=10,
color="systemIndigo",
line_width=2,
fill=False,
)
ctx.gradient_rect(120, 72, 90, 36, colors=["systemPink", "systemMint"], vertical=True)
wave_commands = [
{"op": "fill_rect", "x": 0, "y": 0, "w": 300, "h": 80, "c": "systemGray6"},
]
for i in range(40):
wave_commands.append(
{
"op": "fill_rect",
"x": i * 7.5,
"y": 40 + (i % 5) * 6,
"w": 5,
"h": 12 + (i % 7) * 2,
"c": "systemCyan",
}
)
triangle = appui.Path(
commands=[
{"move": [40, 20]},
{"line": [80, 20]},
{"line": [60, 60]},
{"close": True},
],
fill="systemYellow",
stroke="systemOrange",
line_width=2,
)
return appui.NavigationStack(
appui.ScrollView(
[
appui.VStack(
[
appui.Text("多系列折线 (series)").font("headline"),
appui.Chart(
data=MULTI,
x="day",
y="amount",
type="line",
series="team",
color="systemBlue",
).frame(height=200),
appui.Text("DrawingContext → Canvas").font("headline"),
appui.Canvas(width=300, height=120, context=ctx).corner_radius(12),
appui.Text("commands 列表 → Canvas").font("headline"),
appui.Canvas(width=300, height=80, commands=wave_commands).corner_radius(8),
appui.Text("Path 矢量").font("headline"),
triangle.frame(width=120, height=80),
],
spacing=20,
)
.padding()
]
)
.navigation_title("图表与画布")
)
appui.run(body, state=state)
#常见误区
- 不要把两种层级混在一起:单一图表用
type=;组合图用marks=[...]。Mark 的字符串坐标是字段名,常量用ChartValue。 Canvas命令字典的字段:手写commands时使用'op','x','y','w','h','c'等键。Path命令格式:appui的Path使用{'move': [x, y]}、{'line': [x, y]}、{'close': True}等形式,与DrawingContext的op字典不同。- 性能:高频更新(如波形)可优先
Canvas+ 稳定命令列表;普通业务图表使用Chart。不要从 Python 逐帧改变选择或滚动位置,原生图表已经负责手势和动画。 - 系统版本:
Chart依赖 iOS 16+,SectorMark依赖 iOS 17+,Canvas依赖 iOS 15+;为较低版本保留替代内容。 series与颜色:多系列时color仍可作为默认着色提示,具体调色策略由系统图表渲染决定;若发现图例颜色异常,应先检查数据中series字段是否稳定存在。
#练习题
- 给现有折线增加
RuleMark(y=ChartValue(...)),标注目标阈值。 - 用
DrawingContext绘制一条正弦折线(多点line或stroke_path)。 - 把
Path的commands改为带curve/arc的复杂轮廓(若当前环境支持对应序列化键)。
#附录:最小 Canvas 与 Chart
import appui
c = appui.Canvas(
width=120,
height=40,
commands=[{"op": "fill_rect", "x": 0, "y": 0, "w": 120, "h": 40, "c": "systemBlue"}],
)
g = appui.Chart(
data=[{"x": 1, "y": 2}],
x="x",
y="y",
type="point",
color="systemRed",
)
print(c, g)
#数据字典约定(给 Chart)
Chart 期望 data 为 list[dict]。旧写法要求每行包含 x=、y= 和可选 series= 对应键;组合 Mark 要求每行包含所有 ChartField/字符串坐标引用的键。ChartValue 是常量,不要求数据行存在同名字段。键名建议使用 ASCII 标识符(如 month、sales),可减少跨端序列化问题。
#DrawingContext 生成的 op 一览
下列为 DrawingContext 常用命令,便于手写 commands 数组时对照:
| 链式方法 | op 字段 | 说明 |
|---|---|---|
fill_rect | fill_rect | 填充轴对齐矩形 |
stroke_rect | stroke_rect | 描边矩形 |
fill_circle | fill_circle | 填充圆 |
stroke_circle | stroke_circle | 描边圆 |
fill_ellipse | fill_ellipse | 填充椭圆外接矩形 |
stroke_ellipse | stroke_ellipse | 描边椭圆 |
line | line | 线段 |
fill_text | fill_text | 文本 |
fill_path | fill_path | 多边形填充 |
stroke_path | stroke_path | 折线描边 |
arc | arc | 弧或扇形 |
rounded_rect | rounded_rect | 圆角矩形 |
gradient_rect | gradient_rect | 线性渐变矩形 |