ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Bokeh 自定义扩展实战:通过 wrapping 将第三方 JavaScript 库(vis.js Graph3d)接入 Python 数据管道

Bokeh 自定义扩展实战:通过 wrapping 将第三方 JavaScript 库(vis.js Graph3d)接入 Python 数据管道 Bokeh 自定义扩展实战通过 wrapping 将第三方 JavaScript 库vis.js Graph3d接入 Python 数据管道【免费下载链接】bokehInteractive Data Visualization in the browser, from Python项目地址: https://gitcode.com/GitHub_Trending/bo/bokeh导读本文围绕 Bokeh 官方扩展示例wrapping.py展开讲解如何在不修改 Bokeh 内核的情况下把第三方 JavaScript 库以 vis.js 的 Graph3d 三维图库为例完整“包裹”进 Bokeh 的自定义扩展模型中从而让 NumPy / SciPy / Pandas 等 Python 数据分析工具生成的ColumnDataSource数据实时驱动浏览器端的三维渲染。读完本文你将掌握自定义扩展的 Python 模型与 BokehJS 视图的完整写法、__implementation__与属性镜像机制、TypeScript 内联实现的编译方式以及如何在独立文档与 Bokeh Server 两种场景下运行该扩展。背景为什么需要“包装第三方库”Bokeh 内置了大量开箱即用的图形、标注与交互工具但任何可视化库都无法覆盖所有领域需求。当需要接入一个成熟的三维渲染库、图表库或其他专用前端组件时最省力、最优雅的路径就是把它包装成 Bokeh 自定义扩展。官方 自定义扩展总览 明确指出自定义扩展可以用于三种场景修改既有 Bokeh 模型的行为、新增模型来连接第三方 JavaScript 库与 Python、为特定领域创建高度定制化的模型。本文要讲解的wrapping.py正是第二种场景的典型代表。该示例位于仓库的 examples/advanced/extensions/wrapping.py是 demo.bokeh.org/surface3d。提示Bokeh 官方将扩展机制标注为“高级特性”部分创建与使用环节仍处于活跃开发中可视为实验性功能见 extensions.rst 中的说明。扩展的整体架构Python 模型 BokehJS 视图在 Bokeh 中任何模型都由两半组成Python 侧模型一个基本全声明式的Model子类通过类属性声明会被镜像到 JavaScript 侧的各种属性。JavaScriptBokehJS侧模型与视图负责真正的渲染与交互逻辑。对于要渲染到 DOM 或画布上的模型还必须提供对应的 View 子类。wrapping.py中定义的Surface3d就是这一结构的完整示范Python 类Surface3d继承LayoutDOM使其能够参与 Bokeh 布局系统并通过__implementation__ TypeScript(CODE)把一段内联 TypeScript 字符串作为浏览器侧实现而这段 TypeScript 中又同时定义了Surface3dView继承LayoutDOMView与Surface3d继承LayoutDOM两个类。这段“Python 声明属性、TypeScript 实现行为”的对称结构正是 Bokeh 扩展能自动完成 Python ↔ 浏览器同步的根本原因属性值由 Python 序列化传输到浏览器端BokehJS 端再依据同名属性完成状态恢复与渲染。为什么选择LayoutDOM作为基类注释与官方文档都强调了一个关键点基类的选择决定了扩展的能力边界。Surface3d的 Python 注释wrapping.py写道这个自定义扩展模型的 DOM 视图应当能够在 Bokeh 布局中参与排版因此使用LayoutDOM作为基类。如果你想创建自定义工具可以继承Tool如果你想创建自定义字形可以继承Glyph依此类推。也就是说LayoutDOM→ 可布局的 DOM 组件Tool→ 画布上的交互工具Glyph→ 自定义绘制字形。选对基类是扩展设计的第一步。逐步拆解 Python 侧模型Surface3d的 Python 实现非常精简全部代码如下摘自 wrapping.pyclass Surface3d(LayoutDOM): # 特殊类属性 __implementation__ 存放浏览器侧扩展模型的 JavaScript 代码 __implementation__ TypeScript(CODE) # 一个 Bokeh ColumnDataSource可以在 Bokeh Server 中被 Python 代码更新 data_source Instance(ColumnDataSource) # vis.js 期望接收 x、y、z 三个字段的数据。 # 数据实际存放在 ColumnDataSource 中但下面这些属性 # 让我们可以指定每个字段应该使用哪一列的名称。 x String() y String() z String()__implementation__与内联语言声明__implementation__是扩展模型中最重要的特殊类属性它的取值有两种形式见 extensions.rst 中 “Specifying implementation languages” 一节文件路径形式当值为以.js或.ts结尾的单行字符串时Bokeh 会把它当作文件名读取文件内容并按扩展名决定编译方式。例如另一个官方示例 putting_together.py 就是写__implementation__ custom.ts配合同目录下的 custom.ts 使用。内联代码形式当实现代码直接写在 Python 文件里时需要用JavaScript(...)或TypeScript(...)包装类来显式声明源码语言。wrapping.py使用的正是这种方式__implementation__ TypeScript(CODE)其中CODE是一个三引号字符串。JavaScript与TypeScript包装类定义在 src/bokeh/util/compiler.py它们最终会驱动 Bokeh 内建的编译器把 TypeScript 转译为可运行的 JavaScript 并打包进渲染文档。这意味着你不需要搭建任何前端开发环境——官方文档明确说明使用自定义扩展不需要从源码构建 Bokeh是参与 Bokeh 生态最轻量的方式之一。属性即协议x/y/z的设计巧思data_source、x、y、z四个属性展示了一种非常实用的数据接入模式data_source持有真正的数据列Instance(ColumnDataSource)类型校验由 Bokeh 属性系统自动完成x、y、z三个String属性并不存数据而是声明“数据源中哪一列对应 vis.js 的哪个字段”。这样 Python 端更新数据时只需source.data new_dict浏览器端视图即可从data_source按列名重新取数无需重新构造模型。这种“属性引用列名”的解耦设计让扩展可以复用于任何结构相同的数据源值得在自定义扩展中借鉴。深入 BokehJS 侧TypeScript 视图与模型CODE字符串中的 TypeScript 实现是扩展的核心它完成了三件事在 DOM 中加载第三方库、把 Bokeh 数据适配为第三方库的数据格式、将数据源变更事件转发给第三方库。用 TypeScript 声明第三方库的类型为了让 TypeScript 编译器以及 IDE 智能提示理解尚未加载的 vis.js示例先用declare namespace声明了所需的最小 API 表面wrapping.pydeclare namespace vis { class Graph3d { constructor(el: HTMLElement | DocumentFragment, data: object, OPTIONS: object) setData(data: vis.DataSet): void } class DataSet { add(data: unknown): void } }这只声明了Graph3d与DataSet两个类以及我们真正会用到的setData与add方法——声明范围越小与第三方库的耦合就越低也越容易维护。加载第三方库动态注入 script 标签由于该示例刻意不依赖 Bokeh 的资源声明机制视图的initialize()方法采用运行时动态加载的方式把 vis.js 引入页面wrapping.pyinitialize(): void { super.initialize() const url https://cdnjs.cloudflare.com/ajax/libs/vis/4.16.1/vis.min.js const script document.createElement(script) script.onload () this._init() script.async false script.src url document.head.appendChild(script) }要点设置script.onload回调确保库加载完成后再调用_init()创建图实例script.async false保证脚本按顺序执行代码注释也提到未来 Bokeh 模型将能直接声明并自动加载外部脚本——当前阶段除了这种动态注入方式你还可以使用官方文档介绍的__javascript__与__css__类属性见 extensions.rst 中 “Supplying external resources” 一节在模型层面声明第三方资源例如 LaTeX 渲染库 KaTeX 的LatexLabel示例就是通过这两个属性把 JS 与 CSS 注入文档head。挂载到 shadow DOM 并同步数据库加载完成后_init()创建vis.Graph3d实例并把它挂到视图自带的 DOM 元素上wrapping.pythis._graph new vis.Graph3d(this.shadow_el, this.get_data(), OPTIONS) // 监听 Bokeh 数据源变更事件把新数据转发给 vis.js this.connect(this.model.data_source.change, () { this._graph.setData(this.get_data()) })这里有两处值得注意的细节this.shadow_el而非this.elBokehJS 视图默认会创建div元素即this.el并把它放入 shadow DOM 中this.shadow_el。注释特别说明很多 Bokeh 视图会忽略默认div转而去画布上绘制而本例恰恰利用这个div把 Graph3d 挂进 DOM。使用shadow_el可以隔离第三方库的样式避免污染页面。this.connect(...)是 BokehJS 的信号机制它与 Python 侧on_change等机制对应让扩展视图可以响应模型属性变化。这里监听的是data_source.change一旦 Python 端例如 Bokeh Server 的回调更新了数据浏览器端就会自动把新数据转成 vis.js 的DataSet并重绘。get_data()完成 Bokeh 数据源到 vis.jsDataSet的适配wrapping.pyget_data(): vis.DataSet { const data new vis.DataSet() const source this.model.data_source for (let i 0; i source.get_length()!; i) { data.add({ x: source.get(this.model.x)[i], y: source.get(this.model.y)[i], z: source.get(this.model.z)[i], }) } return data }它按model.x/model.y/model.z指定的列名逐行取值组装成{x, y, z}三元组加入DataSet。此外child_models必须实现并返回空数组[]告知 BokehJS 该视图没有子模型。模型类属性定义与 View 关联TypeScript 侧的Surface3d模型类与 Python 侧一一对应wrapping.pyexport namespace Surface3d { export type Attrs p.AttrsOfProps export type Props LayoutDOM.Props { x: p.Propertystring y: p.Propertystring z: p.Propertystring data_source: p.PropertyColumnDataSource } } export class Surface3d extends LayoutDOM { declare properties: Surface3d.Props declare __view_type__: Surface3dView static __name__ Surface3d static { this.prototype.default_view Surface3dView this.defineSurface3d.Props(({Str, Ref}) ({ x: [ Str ], y: [ Str ], z: [ Str ], data_source: [ Ref(ColumnDataSource) ], })) } }几个关键约定__name__必须与 Python 类名一致。注释特别提醒若使用 TypeScript编译期会自动填入类名一般无需手动编写以免拼写错误导致模型无法序列化/反序列化。default_view把模型与视图关联起来这是“有视图”的模型的固定样板。define()块把 Python 侧声明的属性同步到 JS 侧且两侧类型一一对应bokeh.core.properties.String↔Strbokeh.core.properties.Instance(ColumnDataSource)↔Ref(ColumnDataSource)。当 JS 类型系统不够丰富时可用p.Any作为通配符属性类型上述约定均来自 wrapping.py 与 custom.ts 的注释说明。默认值必须双侧一致官方文档强调出于效率考虑Bokeh 只传输用户显式修改过的属性值因此凡是有默认值的属性Python 侧如Bool(defaultTrue)与 JS 侧如flag: [ Boolean, true ]必须给出相同的默认值否则两侧状态会不一致见 extensions.rst 中 “Specifying default values” 一节。vis.js 的默认配置读懂OPTIONS示例中定义了一组 vis.js Graph3d 的默认选项wrapping.pyconst OPTIONS { width: 600px, height: 600px, style: surface, showPerspective: true, showGrid: true, keepAspectRatio: true, verticalRatio: 1.0, legendLabel: stuff, cameraPosition: { horizontal: -0.35, vertical: 0.22, distance: 1.8, }, }各选项含义选项值作用width/height600px图表 DOM 尺寸与 Python 侧实例化时传入的width600, height600保持一致stylesurface渲染样式Graph3d 支持 surface / dot / bar 等多种风格showPerspectivetrue是否启用透视投影三维立体感showGridtrue是否显示底面网格keepAspectRatiotrue保持三轴比例一致verticalRatio1.0垂直方向的相对比例legendLabelstuff图例标签文本cameraPosition{horizontal: -0.35, vertical: 0.22, distance: 1.8}相机初始位置水平角、仰角、距离决定初始观察视角这些选项在_init()中作为OPTIONS传给new vis.Graph3d(...)。由于它们被定义在 Python 文件的三引号字符串内如果你希望用户可配置完全可以把它们提升为模型的 Bokeh 属性从而获得 Python ↔ 浏览器双向同步——这正是“属性即协议”思想的延伸。端到端运行从数据生成到浏览器渲染独立文档模式本示例的默认方式示例末尾的 Python 代码wrapping.py演示了完整的调用方式x np.arange(0, 300, 10) y np.arange(0, 300, 10) xx, yy np.meshgrid(x, y) xx xx.ravel() yy yy.ravel() value np.sin(xx / 50) * np.cos(yy / 50) * 50 50 source ColumnDataSource(datadict(xxx, yyy, zvalue)) surface Surface3d(xx, yy, zz, data_sourcesource, width600, height600) show(surface)数据生成逻辑在 0~300 的网格上采样步长 10用二维正弦×余弦函数构造起伏曲面再展平成一维数组存入ColumnDataSource。实例化Surface3d时通过xx, yy, zz把列名绑定到三维坐标。运行方式与普通 Bokeh 脚本完全一致python examples/advanced/extensions/wrapping.pyBokeh 会在渲染文档时自动包含内联 TypeScript 的编译产物——无需任何额外安装或前端构建步骤。这也是该示例“不涉及 Bokeh Server”的独立文档形态浏览器打开后即可用鼠标拖拽旋转、缩放查看曲面。Bokeh Server 模式数据实时驱动wrapping.py是精简版但它所对应的完整演示来自仓库中的 Server 应用 examples/server/app/surface3d其目录下的 main.py 展示了同一扩展如何在 Server 中实现周期性实时刷新source ColumnDataSource(datacompute(0)) surface Surface3d(xx, yy, zz, data_sourcesource) curdoc().add_root(surface) count() def update(t): source.data compute(t) curdoc().add_periodic_callback(update, 100)关键点用curdoc().add_root(surface)把扩展模型加入文档用count()驱动的update()每 100ms 更新一次source.data——注意这里只是更新数据源的.data浏览器端data_source.change信号会自动触发_graph.setData(...)重绘这也印证了官方文档的说法自定义扩展与 Bokeh Server 的集成无需任何额外工作模型属性同步机制对自定义扩展与内建模型完全透明见 extensions.rst 中 “Integration with Bokeh server” 一节。运行方式据 surface3d/README.mdcd examples/server/app bokeh serve --show surface3d无需额外安装任何 Python 包。进阶从内联扩展到预构建扩展wrapping.py采用“内联字符串 TypeScript(...)”的轻量方式非常适合快速原型。但官方文档也指出extensions.rst 中 “Pre-built extensions” 一节当扩展进入严肃开发阶段时内联方式存在明显短板隐式的package.json、tsconfig.json配置让你无法充分发挥 IDE 的类型检查、跳转与重构能力。此时应转向预构建扩展pre-built extensionsbokeh init # 生成 bokeh.ext.json、package.json、tsconfig.json 等必要文件 bokeh init --interactive # 分步交互式创建与定制扩展 bokeh build # 按需执行 npm install、编译 TS、转译 JS、解析模块并打包成可分发的 bundle bokeh build --rebuild # 缓存导致问题时从零重新构建其中bokeh build会执行npm install如需要、编译 TypeScript、转译 JavaScript、解析模块依赖并链接成可分发包Bokeh 会缓存编译产物以提升性能遇到缓存导致的问题时可用--rebuild强制全量重建。仓库中 examples/advanced/extensions 目录还提供了其他对照示例帮助你理解不同扩展类型ticking.py自定义TickFormatter定制坐标轴刻度、tool.py继承GestureTool在画布上涂鸦的新工具、widget.py在扩展控件中包含第三方库以及putting_together.py把前文Custom模型组装成可交互页面。它们分别对应官方文档 extensions.rst 中扩展示例列表里的四类能力。小结与最佳实践清单回到本文主题——“包装 JavaScript 库”总结出可复用的实施要点选对基类需要参与 Bokeh 布局的 DOM 组件继承LayoutDOM工具继承Tool字形继承Glyph。Python 侧只声明属性用Instance(ColumnDataSource)接数据用String属性声明列名映射__implementation__使用文件路径.ts/.js或TypeScript(...)/JavaScript(...)内联包装。JS 侧模型与视图对称__name__与 Python 类名一致TypeScript 下可省略、default_view关联视图、define()块与 Python 属性一一对应默认值两侧必须一致。第三方库加载既可用__javascript__/__css__类属性声明资源自动注入head也可在视图initialize()中动态注入script并配合onload回调挂载点建议使用this.shadow_el隔离样式。数据同步通过this.connect(this.model.data_source.change, ...)响应数据变更把ColumnDataSource适配为第三方库所需格式如 vis.js 的DataSet。运行形态独立文档直接show(surface)Server 模式curdoc().add_root(...)加周期性回调更新source.data无需额外集成工作。工程化从原型走向产品时用bokeh init/bokeh build切换到预构建扩展获得 IDE 与构建管道的完整支持。按照以上模式你可以把任意具备浏览器 API 的第三方库图表、地图、3D、富文本等无缝接入 Bokeh并让 Python 端的数据分析能力直接驱动其渲染——这正是 Bokeh 扩展机制最有价值的使用场景。【免费下载链接】bokehInteractive Data Visualization in the browser, from Python项目地址: https://gitcode.com/GitHub_Trending/bo/bokeh创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表