如何在Elixir、Phoenix、SurfaceUI中渲染VegaLite图表?
在Elixir、Phoenix、SurfaceUI组件中渲染VegaLite图表
问题背景
我需要在Surface LiveView组件中渲染VegaLite图表,已经能生成VegaLite的spec结构,但不知道如何把它渲染到页面上。目前的尝试代码如下:
defmodule CfiAppWeb.Dashboard do @moduledoc """ LiveView for the dashboard """ use Surface.LiveView alias VegaLite, as: Vl def render(assigns) do ~F""" <main class="container mx-auto p-4" > <div> <h1>This is dashboard</h1> <div>{render_graph()}</div> </div> </main> """ end def render_graph() do Vl.new(width: 400, height: 400) |> Vl.mark(:line) |> Vl.encode_field(:x, "x", type: :quantitative) |> Vl.encode_field(:y, "y", type: :quantitative) end end
通常在纯JS环境里可以通过DOM ID渲染Vega图表,但在Surface LiveView场景下找不到合适的方法。
解决方案
要在Surface中渲染VegaLite图表,核心是把Elixir侧生成的VegaLite spec转换成JSON,然后在前端用vega-embed库渲染到指定DOM元素。具体步骤如下:
1. 添加前端依赖
首先在项目的package.json中添加vega相关依赖:
{ "dependencies": { "vega": "^5.25.0", "vega-embed": "^6.22.2", "vega-lite": "^5.12.0" } }
运行npm install完成依赖安装。
2. 修改Surface组件,传递JSON格式的spec
修改LiveView代码,将VegaLite spec转换成JSON字符串,给图表容器指定唯一ID,并添加Surface的phx-hook标记触发前端渲染逻辑:
defmodule CfiAppWeb.Dashboard do @moduledoc """ LiveView for the dashboard """ use Surface.LiveView alias VegaLite, as: Vl def render(assigns) do ~F""" <main class="container mx-auto p-4" > <div> <h1>This is dashboard</h1> <div id="vega-chart" phx-hook="VegaEmbed" data-spec={Jason.encode!(render_graph())}></div> </div> </main> """ end def render_graph() do Vl.new(width: 400, height: 400) |> Vl.data_from_values(x: 1..100, y: 1..100) # 补充数据源,原代码缺少数据配置 |> Vl.mark(:line) |> Vl.encode_field(:x, "x", type: :quantitative) |> Vl.encode_field(:y, "y", type: :quantitative) end end
3. 编写Phoenix Hook处理渲染逻辑
在assets/js/app.js中添加自定义Phoenix Hook,调用vega-embed完成图表渲染:
import { Socket } from "phoenix"; import { LiveSocket } from "phoenix_live_view"; import topbar from "../vendor/topbar"; import vegaEmbed from "vega-embed"; let Hooks = {}; Hooks.VegaEmbed = { mounted() { // 解析DOM上的spec数据并渲染图表 const spec = JSON.parse(this.el.dataset.spec); vegaEmbed(this.el, spec) .then(result => { // 保存视图实例,方便后续更新 this.view = result.view; }) .catch(console.error); }, updated() { // 当LiveView更新时,重新加载新的spec const spec = JSON.parse(this.el.dataset.spec); this.view.change(spec); } }; let liveSocket = new LiveSocket("/live", Socket, { params: { _csrf_token: window.csrfToken }, hooks: Hooks }); // ... 保留文件中其他原有代码
4. 动态数据更新(可选)
如果需要根据LiveView的状态动态更新图表数据,可以修改组件实现数据联动:
def mount(_params, _session, socket) do # 初始化图表数据 socket = assign(socket, chart_data: generate_random_data()) {:ok, socket} end def render(assigns) do ~F""" <main class="container mx-auto p-4" > <div> <h1>This is dashboard</h1> <div id="vega-chart" phx-hook="VegaEmbed" data-spec={Jason.encode!(render_graph(@chart_data))}></div> <button phx-click="refresh_data" class="mt-4 px-4 py-2 bg-blue-500 text-white rounded">刷新数据</button> </div> </main> """ end def handle_event("refresh_data", _, socket) do # 更新图表数据 socket = assign(socket, chart_data: generate_random_data()) {:noreply, socket} end # 生成随机数据示例 defp generate_random_data() do 1..100 |> Enum.map(fn x -> %{x: x, y: :rand.uniform(100)} end) end defp render_graph(data) do Vl.new(width: 400, height: 400) |> Vl.data_from_values(data) |> Vl.mark(:line) |> Vl.encode_field(:x, "x", type: :quantitative) |> Vl.encode_field(:y, "y", type: :quantitative) end
点击"刷新数据"按钮时,LiveView会更新图表数据,前端Hook的updated方法会自动重新渲染图表。
内容的提问来源于stack exchange,提问作者suppa
相关产品推荐
相关产品推荐

