note Reflex 是一个开源库,旨在使用纯 Python 构建全栈 Web 应用程序。它的设计理念是让开发者能够完全使用 Python 编写应用的前端和后端,无需学习 JavaScript。这对于那些希望专注于 Python 编程语言的开发者来说是一个重大的福音。
上面说的都是场面话,这都依托于它自身包装的第三方应用够丰富。但是,对于一些冷门的应用,比如我这里要说的IGV浏览器,就没有那么容易了。这里我就来分享一下我是如何在 Reflex 里面使用 IGV 浏览器的。
包装igv.js到react组件
首先,我们需要把 igv.js 包装成一个 React 组件。我们以两个需要实现的交互为例子来说明:
- 第一个是我们有一个外部的控制器,可以通过这个控制器来控制 igv.js 显示的坐标范围。
- 第二个是当我们鼠标点击某个区域的时候,我们可以获取到这个区域的所有信息。
首先,通过查阅 IGV.js的文档,我们知道,我们可以通过 igv.createBrowser
来创建一个 IGV 浏览器。我们可以通过这个函数来创建一个 IGV 浏览器,然后通过这个浏览器的实例来控制 IGV 的显示。
import igv from "https://unpkg.com/igv@3.0.0/dist/igv.esm.min.js"
const div = document.getElementById("igv_div")
const config = { genome: "hg19" }
const browser = await igv.createBrowser(div, config)
|
然后我们可以通过igv.removeAllBrowsers()
来移除所有的浏览器。
那么,我们放到 React 组件里面,就是这样的:
import React, { useEffect, useRef } from "react"; import igv from "igv";
const IgvComponent = () => { const igvContainerRef = useRef(null); const browserRef = useRef(null); useEffect(()=>{ const options = { genome: 'hg19', }; igv.createBrowser(igvContainerRef.current, options).then(browser => { browserRef.current = browser; }); }); return ( <> <div ref={igvContainerRef} />; </> ); };
export default IgvComponent;
|
现在,我们来添加交互,我们想要实现的第二个功能需要 browser.on
来监听事件。比如我们想要监听鼠标点击事件,我们可以这样:
browser.on("trackclick", (track, popoverData) => {})
|
我们放入 React 组件里面,就是这样的:
import React, { useEffect, useRef } from "react"; import igv from "igv";
const IgvComponent = (genome, locus, tracks, onTrackClick) => { const igvContainerRef = useRef(null); const browserRef = useRef(null); useEffect(()=>{ const options = { genome, locus, tracks, }; igv.createBrowser(igvContainerRef.current, options).then(browser => { browserRef.current = browser; if (onTrackClick) { browser.on("trackclick", (track, popoverData) => { const trackName = track.name; const trackValue = popoverData.map((data) => ({ name: data.name, value: data.value, })); onTrackClick(trackName, trackValue); }); } }); }); return ( <> <div ref={igvContainerRef} />; </> ); };
export default IgvComponent;
|
而第一个要求里,我们要外部控制,需要获取到browser的实例,然后通过这个实例来控制 IGV 的显示。我们可以通过 browser.search
来搜索某个基因,通过 browser.zoomIn
和 browser.zoomOut
来缩放。这在react里是这样的:
import React, { useEffect, useRef } from "react"; import igv from "../node_modules/igv/dist/igv.esm.min.js";
const IgvComponent = ({ genome, locus, tracks, onTrackClick }) => { const igvContainerRef = useRef(null); const browserRef = useRef(null);
useEffect(() => { const options = { genome, locus, tracks, };
if (!browserRef.current) { igv.createBrowser(igvContainerRef.current, options).then((browser) => { browserRef.current = browser; if (onTrackClick) { browser.on("trackclick", (track, popoverData) => { const trackName = track.name; const trackValue = popoverData.map((data) => ({ name: data.name, value: data.value, })); onTrackClick(trackName, trackValue); }); } }); }
return () => { if (browserRef.current) { igv.removeAllBrowsers() } }; }, []);
useEffect(() => { if (browserRef.current) { browserRef.current.search(locus); } }, [locus]);
return <div ref={igvContainerRef} />; };
export default IgvComponent;
|
这样,我们就完成了最终的包装。我们可以自己搭建React环境来测试一下是否可行,或者可以去找在线平台测试,例如playcode
在 Reflex 里面使用
根据官方文档的表述,React组件的包装就相当简单了,我们这里是本地的组件,所以就可以这样:
import reflex as rx from typing import List, Dict from reflex.components.component import NoSSRComponent
class IGV(NoSSRComponent): library = "/public/igv"
tag = "IgvComponent"
is_default = True
lib_dependencies: list[str] = [ "igv@3.0.6" ]
genome: rx.Var[str] locus: rx.Var[str] tracks: rx.Var[List[Dict[str, str]]]
on_track_click: rx.EventHandler[lambda e0, e1: [e0, e1]]
igvComponent = IGV.create
|
一定要注意的是,Reflex默认是服务端渲染的,但是显然 IGV浏览器无法在服务端兼容,那么我们就是用 NoSSRComponent
来包装。
如何使用?
我们可以写一个小的 Demo来测试一下:
import reflex as rx
from .IGV import igvComponent
class State(rx.State): """The app state.""" genome: str = "hg38" loc: str = "chr8:127736588-127739371" tracks: list[dict[str, str]] = [ { "name": "HG00103", "url": "https://s3.amazonaws.com/1000genomes/data/HG00103/alignment/HG00103.alt_bwamem_GRCh38DH.20150718.GBR.low_coverage.cram", "indexURL": "https://s3.amazonaws.com/1000genomes/data/HG00103/alignment/HG00103.alt_bwamem_GRCh38DH.20150718.GBR.low_coverage.cram.crai", "format": "cram" }] location: str trackName: str trackdata: list[dict[str, str]]
def change_loc(self): self.loc = self.location
def get_click_info(self, e0: str, e1: list[dict[str, str]]): self.trackName = e0 self.trackdata = [{"name": itm['name'], "value": itm['value']} for itm in e1 if itm['name']]
def data_item(name: str, value: str) -> rx.Component: return rx.data_list.item( rx.data_list.label(name), rx.data_list.value(value) )
def data_card(trackdata: list[dict[str, str]]) -> rx.Component: return rx.card( rx.data_list.root( rx.foreach(trackdata, lambda item: data_item( item['name'], item['value'])) ) )
def index() -> rx.Component: return rx.container( rx.vstack( rx.heading("IGV test!", size="9"), rx.box( igvComponent(genome=State.genome, locus=State.loc, tracks=State.tracks, on_track_click=State.get_click_info), width="100%", ), rx.hstack( rx.input(placeholder="Enter location", on_blur=State.set_location, width="40%"), rx.button("GoTo!", on_click=State.change_loc), width="100%", justify="center" ), rx.divider(), rx.cond( State.trackName, rx.vstack( rx.heading(State.trackName, size="4"), data_card(State.trackdata) ), rx.heading("No track clicked", size="4") ),
spacing="5", justify="center", min_height="85vh", ), rx.logo() )
app = rx.App() app.add_page(index)
|
使用就很简单了,一旦包装好,我们导入它就可以当做官方的组件来使用了,我们可以为他写 State,写 EventHandler,写页面,写交互,写样式,写逻辑,写一切。
总结
这里我们介绍了如何将 igv.js 包装成 React 组件,然后将 React 组件包装成 Reflex 组件,最后在 Reflex 里面使用。这里只是一个简单的例子,实陿上,我们可以包装任何我们想要的组件,只要我们有足够的耐心和技术。本文的代码在 Github 可以获取。
note 这里的 igv.js 是一个简单的例子,实陿上,我们可以包装任何我们想要的组件,只要我们有足够的耐心和技术。