PythonIDE Docs
中文
简体中文

图表与画布

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_axesvisible_domainselectionon_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:水平或垂直参考线,适合阈值、平均线等「标线」场景(数据需自行构造为常数 yx)。

#生产级参数

  • interpolationlinearmonotonecardinalcatmull_romstep_startstep_centerstep_end
  • stackingstandardnormalizedcenterunstacked
  • symbol / symbol_size:折线与散点的系统符号。
  • show_legendshow_x_axisshow_y_axisy_domain:图例、坐标轴和固定数值域。
  • selection=state.bind.x:触摸选择与 Python 状态双向同步;on_select(row) 得到最接近的原始数据字典。
  • scrollable_axes='horizontal' + visible_domain=...:大于屏幕宽度的数据使用系统图表滚动。
  • annotation_key:把某个字段作为原生 mark 标注。
  • marks:1–32 个 Mark 图层;可用 BarMarkLineMarkAreaMarkPointMarkRuleMarkRectangleMarkSectorMark
  • selection_x:组合 Mark 用于触摸选择和最近数据行匹配的字段;省略时沿用 x

#什么时候使用组合 Mark

一种图形、统一颜色、统一插值时,旧的 type= 写法更短,也会长期兼容。以下情况用 marks=

  • 同一图中叠加面积、折线、散点与阈值。
  • 需要区间柱、范围面积或 RectangleMark
  • 每个图层需要独立颜色、透明度、线宽、虚线、symbol 或标注。
  • 需要 iOS 17+ 的 SectorMark 饼图/环形图。

Mark 坐标中的字符串表示数据字段。要表达常量,尤其是常量字符串,使用 ChartValue;需要明确轴标题或字段类型时使用 ChartField

python
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 两条路径

  1. 声明式DrawingContext 链式调用,最后交给 Canvas(..., context=ctx);可读性高,适合中等数量图元。
  2. 命令列表:直接维护 commands 列表,适合波形、频谱等高频追加场景。

#基础示例

同一组销售数据,切换 Charttype

python
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 原始 commandsPath 三角形。

python
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)

#常见误区

  1. 不要把两种层级混在一起:单一图表用 type=;组合图用 marks=[...]。Mark 的字符串坐标是字段名,常量用 ChartValue
  2. Canvas 命令字典的字段:手写 commands 时使用 'op', 'x', 'y', 'w', 'h', 'c' 等键。
  3. Path 命令格式appuiPath 使用 {'move': [x, y]}{'line': [x, y]}{'close': True} 等形式,与 DrawingContextop 字典不同。
  4. 性能:高频更新(如波形)可优先 Canvas + 稳定命令列表;普通业务图表使用 Chart。不要从 Python 逐帧改变选择或滚动位置,原生图表已经负责手势和动画。
  5. 系统版本Chart 依赖 iOS 16+,SectorMark 依赖 iOS 17+,Canvas 依赖 iOS 15+;为较低版本保留替代内容。
  6. series 与颜色:多系列时 color 仍可作为默认着色提示,具体调色策略由系统图表渲染决定;若发现图例颜色异常,应先检查数据中 series 字段是否稳定存在。

#练习题

  1. 给现有折线增加 RuleMark(y=ChartValue(...)),标注目标阈值。
  2. DrawingContext 绘制一条正弦折线(多点 linestroke_path)。
  3. Pathcommands 改为带 curve / arc 的复杂轮廓(若当前环境支持对应序列化键)。

#附录:最小 CanvasChart

python
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 期望 datalist[dict]。旧写法要求每行包含 x=y= 和可选 series= 对应键;组合 Mark 要求每行包含所有 ChartField/字符串坐标引用的键。ChartValue 是常量,不要求数据行存在同名字段。键名建议使用 ASCII 标识符(如 monthsales),可减少跨端序列化问题。


#DrawingContext 生成的 op 一览

下列为 DrawingContext 常用命令,便于手写 commands 数组时对照:

链式方法op 字段说明
fill_rectfill_rect填充轴对齐矩形
stroke_rectstroke_rect描边矩形
fill_circlefill_circle填充圆
stroke_circlestroke_circle描边圆
fill_ellipsefill_ellipse填充椭圆外接矩形
stroke_ellipsestroke_ellipse描边椭圆
lineline线段
fill_textfill_text文本
fill_pathfill_path多边形填充
stroke_pathstroke_path折线描边
arcarc弧或扇形
rounded_rectrounded_rect圆角矩形
gradient_rectgradient_rect线性渐变矩形