• 作者:老汪软件技巧
  • 发表时间:2024-09-14 04:00
  • 浏览量:

构建交互式 web 应用可能看起来有点复杂,但借助 Bokeh,其实我们可以轻松地使用 Python 创建动态的数据驱动应用!在本教程中,我将逐步指导你创建一个可视化用户移动的交互式 web 应用。我们的简单应用将包含:

通过本教程,你将拥有一个功能齐全的交互式 web 应用,利用 Bokeh 的小部件、图表和回调。无论你是 Bokeh 的新手还是希望探索其高级功能的用户,本教程将为你提供构建交互式数据可视化的实践经验。

让我们开始吧!

环境设置

要构建我们的交互式 Bokeh 应用,我们首先需要设置一个 Python 环境,并安装必要的包。我们将使用 Pipenv 进行虚拟环境和依赖管理。

假设你已经安装了 Python。如果没有,请安装 Python,推荐使用 3.12 版本。

第一步:安装 Pipenv

如果你还没有安装 Pipenv,可以通过 pip 安装:

pip install pipenv

第二步:设置虚拟环境

导航到你的项目目录,并运行以下命令以创建一个新的虚拟环境:

pipenv --python 3.12.6

第三步:定义依赖项

接下来,创建一个 Pipfile 来管理项目的依赖项。以下是我们项目的 Pipfile 内容:

[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
bokeh = "*"
pandas = "*"
numpy = "*"
[dev-packages]
[requires]
python_version = "3.12.6"

第四步:安装依赖项

在 Pipfile 文件准备好后,通过运行以下命令安装依赖项:

pipenv install

该命令将创建虚拟环境并安装 Pipfile 中指定的所有所需包。

准备数据

在本教程中,我们将使用 CSV 文件来简化数据处理。每个用户将有自己的 CSV 文件,包含时间戳轨迹数据。数据将包括时间戳(Unix 格式)和位置坐标(纬度和经度)。

以下是 CSV 文件的示例结构:

timestamp,user_id,latitude,longitude
1694130000,User1,37.7749,-122.4194
1694130001,User1,37.7750,-122.4195
1694130002,User1,37.7751,-122.4196

虽然在生产应用中更常使用数据库连接,但为了简化教程,我们将使用 CSV 文件。

主要 Python 代码

现在,让我们开始构建应用。我们将设置小部件和布局。我们需要的主要组件是:

下拉菜单 用于加载用户数据,地图小部件 用于可视化用户的轨迹,以及播放控制 用于与数据互动。

以下是设置小部件的示例代码:

#region ---------------------- 设置小部件 ----------------------
geo_plot = figure(
    height=550,
    width=1500,
    x_range=(-2000000, 2000000), 
    y_range=(1000000, 7000000),
    x_axis_type="mercator", 
    y_axis_type="mercator",
    active_scroll='wheel_zoom'
)
geo_plot.add_tile("CartoDB Positron", retina=True)
# 时间滑块
slider_time = Slider(start=0, end=100, value=0, step=1, title="", width=700)
# 播放控制
button_reset_state = Button(label="||◄◄", button_type="default", width=100)
button_play = Button(label="► Play", button_type="default", width=100)
# 用户选择下拉菜单
users_menu = ["user1", "user2", "user3"]
dropdown_Users = Dropdown(label="加载用户轨迹", button_type="warning", menu=users_menu, width=250)
#endregion
#region ---------------------- 设置布局并添加到文档 ----------------------
doc_layout = column(
    dropdown_Users,
    geo_plot,
    slider_time, 
    row(button_reset_state, button_play)
)
curdoc().add_root(doc_layout)
curdoc().title = "播放轨迹数据"
#endregion

接下来,我们需要设置数据格式和读取数据源的方法,并将数据重新组织成正确的格式。

#region ---------------------- 设置数据 ----------------------
# 坐标转换为 Web Mercator
def latlon_to_mercator(lat, lon):
    """
    将纬度和经度转换为 Mercator 投影坐标。
    参数:
    lat (float): 纬度(度)。
    lon (float): 经度(度)。
    返回:
    tuple: Mercator 投影的 x 和 y 坐标(米)。
    """
    # 常量
    RADIUS = 6378137  # 地球半径(米,WGS84)
    ORIGIN_SHIFT = 2 * math.pi * RADIUS / 2.0
    
    # 将纬度和经度从度转换为弧度
    lat_rad = math.radians(lat)
    lon_rad = math.radians(lon)
    
    # Mercator 投影公式
    x = lon_rad * RADIUS
    y = math.log(math.tan(math.pi / 4 + lat_rad / 2)) * RADIUS
    
    return (x, y)
# 初始化数据源
trajectory = {'x': [], 'y': [], 'unix_s': []} 
live_position_data = ColumnDataSource(data={'x': [], 'y': []})
def loadUserTrajectoryData(event):
    # 加载所选用户的 CSV 文件
    user_id = event.item
    user_file = f"Data/{user_id}_trajectory.csv"
    data = pd.read_csv(user_file)
    
    # 清除之前的数据
    trajectory['x'].clear()
    trajectory['y'].clear()
    
    # 将纬度/经度转换为 Mercator 以用于地图绘制
    for _, row in data.iterrows():
        lat, lon = row['latitude'], row['longitude']
        merc_x, merc_y = latlon_to_mercator(lat, lon)
        trajectory['x'].append(merc_x)
        trajectory['y'].append(merc_y)
        trajectory['unix_s'].append(row['timestamp'])
    
    # 初始化图表
    plot_session_init_data()
def plot_session_init_data():
    # 使用初始数据更新 ColumnDataSource
    live_position_data.data = {'x': trajectory['x'], 'y': trajectory['y']}
    geo_plot.line(trajectory['x'], trajectory['y'], line_width=2, color="black", line_alpha=0.3, legend_label='轨迹')
    zoom_to_trajectory()
    # 设置用户的初始位置(第一个点)
    if trajectory['x'] and trajectory['y']:
        live_position_data.data = {'x': [trajectory['x'][0]], 'y': [trajectory['y'][0]]}
        geo_plot.scatter(x="x", y="y", size=10, fill_color="deepskyblue", fill_alpha=0.6, line_color="lightsteelblue", line_width=4,
                                source=live_position_data, level='overlay', name='fusedPos', legend_label='实时位置')
def zoom_to_trajectory():
    buff = 200
    aspectRatio = 550 / 1500
    # 计算轨迹的边界(最小值和最大值)
    min_x, max_x = min(trajectory['x']), max(trajectory['x'])
    min_y, max_y = min(trajectory['y']), max(trajectory['y'])
    xRange = abs(max_x - min_x)
    yRange = abs(max_y - min_y)
    map_range = max(xRange, yRange) / 2.0
        
    centerX = (min_x + max_x) / 2
    centerY = (min_y + max_y) / 2
    
    # 设置图表的 x_range 和 y_range 以缩放到轨迹
    geo_plot.x_range.start = centerX - map_range - buff
    geo_plot.x_range.end = centerX + map_range + buff
    geo_plot.y_range.start = centerY - (map_range + buff) * aspectRatio
    geo_plot.y_range.end = centerY + (map_range  + buff) * aspectRatio
#endregion

到目前为止,你应该能够选择用户并查看其轨迹。

最后,我们需要添加回调,使滑块和播放功能能够正常工作。

#region ---------------------- 设置回调 ----------------------
def reset_state():
    global state
    state = slider_time.value
    state = min(trajectory['unix_s'])
    slider_time.value = state
waitTime = 200
def animate():
    global callback_id, state
   
 cur_time = time.time() * 1000
    time_step = slider_time.value * waitTime
    if slider_time.value < len(trajectory['unix_s']):
        slider_time.value += 1
        # 更新用户位置
        state = trajectory['unix_s'][slider_time.value]
        cur_x = trajectory['x'][slider_time.value]
        cur_y = trajectory['y'][slider_time.value]
        # 更新图表
        live_position_data.data = {'x': [cur_x], 'y': [cur_y]}
    else:
        # 重置播放
        slider_time.value = 0
        button_play.label = '► Play'
        curdoc().remove_periodic_callback(callback_id)
        callback_id = None
def update_time_slider():
    if button_play.label == '► Play':
        button_play.label = '⏸️ Pause'
        callback_id = curdoc().add_periodic_callback(animate, waitTime)
    else:
        button_play.label = '► Play'
        curdoc().remove_periodic_callback(callback_id)
        callback_id = None
# 绑定回调
dropdown_Users.on_click(loadUserTrajectoryData)
button_reset_state.on_click(reset_state)
button_play.on_click(update_time_slider)
#endregion

完成上述步骤后,你应该拥有一个功能完整的交互式 Bokeh 应用。

运行应用程序

要运行应用程序,请将代码保存到一个文件中,例如 TrajectoryDataVis.py,然后使用以下命令:

pipenv run bokeh serve --show TrajectoryDataVis.py

这将启动 Bokeh 服务器,打开默认的 Web 浏览器,并显示交互式 Web 应用程序。

结论

恭喜你完成了这个教程!通过这些步骤,你已经学习了如何使用 Bokeh 构建交互式 Web 应用,专注于响应式数据可视化。

核心概念类似于 ReactJS 等框架:管理数据的状态。在本教程中,我们关注了三个关键状态:

轨迹数据:在加载新用户数据时更新。当前时间:在播放过程中调整,以反映当前时间点。实时位置:随着时间的推移不断更新,显示用户的当前位置。

掌握了这些原则,Bokeh 允许你创建动态和响应式的可视化。我希望你发现这个教程既有趣又富有启发性。你可以使用 Bokeh 添加更多炫酷的功能,以构建更强大的仪表板。为了进一步探索,我鼓励你深入阅读[官方 Bokeh 文档],获取更多示例,并发现如何自定义和扩展你的交互式仪表板。

祝编码愉快!