吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1489|回复: 7
收起左侧

[Python 原创] bokeh库实现项目计划可视化

[复制链接]
lianxiang1122 发表于 2024-3-27 16:33
bokeh库是一个十分强大的可视化库,生成html文件实现互动式可视化。
模拟一个简单的阀门安装计划。

录制_2024_03_27_15_13_28_346.gif

首先,利用Python模拟出数据。
import pandas as pd
import random
from datetime import datetime, timedelta

# 生成随机字母
def random_letter():
    return random.choice('ABCDEFGHIJKLMNOPQRSTUVWXYZ')

# 生成随机数字
def random_number():
    return ''.join(random.choices('0123456789', k=4))

# 生成随机日期(只有年月日)
def random_date(start_date, end_date):
    delta = end_date - start_date
    random_days = random.randint(0, delta.days)
    return start_date + timedelta(days=random_days)

# 创建日期范围
start_date_delivery = datetime(2024, 4, 1)
end_date_delivery = datetime(2024, 6, 30)

start_date_installation = datetime(2024, 5, 1)
end_date_installation = datetime(2024, 8, 31)

# 创建数据列表
data = []

for i in range(1, 1201):
    valve_number = f"G{random_letter()}V-{random_number()}"
    delivery_date = random_date(start_date_delivery, end_date_delivery)
    installation_date = random_date(start_date_installation, end_date_installation)
    # 确保到货日期早于安装日期
    while delivery_date >= installation_date:
        delivery_date = random_date(start_date_delivery, end_date_delivery)
        installation_date = random_date(start_date_installation, end_date_installation)

    data.append([i, valve_number, delivery_date.strftime("%Y-%m-%d"), installation_date.strftime("%Y-%m-%d"),1])

# 创建DataFrame
df = pd.DataFrame(data, columns=['No', 'Tag', 'ETA', 'VIP','Qty']) 
#No 序号
#Tag 阀门编号
#ETA 到货计划
#VIP 阀门安装计划
#Qty 数量

# 保存为Excel文件
df.to_excel('valve_schedule.xlsx', index=False)


再根据数据模拟出文件(利用一个pdf文件,创建出一些模拟图纸),如下:
屏幕截图 2024-03-27 152134.png

import os
import shutil

def create_folders_and_copy_file(folder_names, base_directory, file_to_copy):
    for folder_name in folder_names:
        folder_path = os.path.join(base_directory, folder_name)
        os.makedirs(folder_path, exist_ok=True)
        print(f"Created folder: {folder_path}")

        # 提取列表中的名称
        file_name_prefix = folder_name

        # 复制文件到新创建的文件夹并命名
        for suffix in ['总图', '布置图', '详图', '底座图纸']:
            new_file_name = f"{file_name_prefix}{suffix}.pdf"
            destination_file_path = os.path.join(folder_path, new_file_name)
            shutil.copy(file_to_copy, destination_file_path)
            print(f"Copied file {file_to_copy} to {destination_file_path}")

# 定义文件夹名称列表、基础目录和要复制的文件路径
folder_names = list(df.Tag)
base_directory = 'pic'
file_to_copy = '1.pdf'

# 调用函数创建文件夹并复制文件
create_folders_and_copy_file(folder_names, base_directory, file_to_copy)


数据准备好了就可以搞了。

首先登录官网,按照提示进行安装。
https://docs.bokeh.org/en/latest/docs/first_steps/installation.html

pip install bokeh

安装完之后,可以根据官网的学习平台学习,点击网页上方的Turorial就可以进入学习界面了。
屏幕截图 2024-03-27 152758.png

我们就不废话了,直接上代码。

首先,用pandas处理下数据。

import pandas as pd

# 读取电子表格中的Sheet1数据
df = pd.read_excel('valve_schedule.xlsx', sheet_name='Sheet1',engine = 'calamine')

# 打印Sheet1数据的前几行
print(df.head())
#按日期统计数量

简单说一下“calamine”这个引擎,比openpyxl快了很多倍,需要安装。
 pip install python-calamine -i https://pypi.tuna.tsinghua.edu.cn/simple/

df['VIP'] = pd.to_datetime(df['VIP'])
max_date = df['VIP'].max()
min_date = df['VIP'].min()
df['VIP'] = df['VIP'].dt.strftime('%Y-%m-%d')
#按日期统计数量
date_quantity_install = df.groupby('VIP')['Qty'].agg(sum).reset_index()
x_date_install = list(date_quantity_install.VIP.array)
y_quantiy_install = list(date_quantity_install.Qty.array)

#按日期统计Tag号
date_tags = df.groupby('VIP')['Tag'].agg(lambda x: '<br>'.join(x)).reset_index()
z_tag_install = list(date_tags['Tag'])


不做解释了,比较基础一些数据清洗。
下面该bokeh上场了。
先将数据转成bokeh特有的数据类型,这个在官方教程里有介绍。然后又创建了一个柱状图,但是此时柱状图里没有数据。
from bokeh.models import ColumnDataSource
from bokeh.plotting import figure

#将数据转成DataSource,bokeh特有的数据类型。
source_install = ColumnDataSource(data = {'x': x_date_install,
                                  'y': y_quantiy_install,
                                 'z':z_tag_install,}
                         )

# 创建柱状图
plot_install_plan = figure( x_range=source_install.data["x"],
                                title="安装计划",
                                #width = 1000,
                                height=350,
                                tooltips=[("日期", " @x"),("数量", " @y"),("位号", " @z{safe}"),],
                               toolbar_location="above",

                            )


将数据添加到柱状图里。
# 添加柱状图
bars = plot_install_plan.vbar( 
                        x="x",
                        top="y",
                        source=source_install,
                        #legend_label="安装计划",
                        width=0.6,
                                        )

# 在柱状图上添加文本标签
plot_install_plan.text(x='x', y='y', text='y', text_align='center', text_color = 'red',
       text_baseline='bottom', source=source_install)


最后再简单优化一下显示,就可以show一下了。
# customize the appearance of the grid and x-axis
from bokeh.models import NumeralTickFormatter
from bokeh.plotting import show
plot_install_plan.xgrid.grid_line_color = None
plot_install_plan.yaxis.formatter = NumeralTickFormatter(format="0,0")
plot_install_plan.xaxis.major_label_orientation = 0.8  # rotate labels by roughly pi/4

show(plot_install_plan)


结果是这样的,与上面的gif还差一些。。。
屏幕截图 2024-03-27 154846.png

差啥呢?最上方的文本,下方的拖动轴,左侧的显示。

首先,上方的文本好解决,加一个DIV就行了,show的时候用column组合一下。

from bokeh.models import Div
from bokeh.layouts import column

div_top = Div(text="""<h3>某某项目: </h3><a href="https://en.wikipedia.org/wiki/HTML">项目名称</a>位于某某地方,计划投资某某亿,工期一年。。。。
<li>项目工期:2024年1月-2024年10月</li>  <li>项目地址:某某市某某区</li> <li>质量第一,安全第一!</li>""",
align = 'start',height=120)
#.......

show(column(div_top,plot_install_plan))


屏幕截图 2024-03-27 155354.png

再在下方加一个拖动条,需要用到RangeSlider。
from bokeh.models import RangeSlider
#增加一个拖动条
range_slider = RangeSlider(
    title="Adjust x-axis range",
    start=1,
    end=(max_date-min_date).days,
    step=1,
    value=(35, 45),
)
range_slider.js_link("value", plot_install_plan.x_range, "start", attr_selector=0)
range_slider.js_link("value", plot_install_plan.x_range, "end", attr_selector=1)

show(column(div_top,plot_install_plan,range_slider))


屏幕截图 2024-03-27 155717.png

最麻烦的就是左侧显示了,显示是根据鼠标所在的触发位置信息显示,这就需要用到callback了,这个官方教程里没有详细介绍,这个跟html还相关,自己研究研究吧。
from bokeh.models import CustomJS, Div, TapTool

# 添加 Tap 工具
taptool = TapTool()
plot_install_plan.add_tools(taptool)
#创建一个div
div = Div(width=150)
# 定义点击事件的 JavaScript 回调
callback = CustomJS(args=dict(div=div,source=bars.data_source,x_axis=plot_install_plan.xaxis[0],y_data=bars.data_source.data['z'],title = '安装计划',
                             ), code="""

            // 获取绘图数据
            var data = source.data;
            // 获取x值
            var x = data['x'];
             var indices = cb_data.source.selected.indices;
            console.log("点击了索引:" + indices);
            var indices = cb_data.source.selected.indices;
            console.log("点击了索引:" + indices);
            var y_indices = [];
            for (var i=0; i<indices.length; i++) {
                y_indices.push(indices[i]);
            }
            var y_values = [];
            for (var i=0; i<indices.length; i++) {
                y_values.push(y_data[indices[i]]);
            }
            div.text =`${title}:<br><br>${x[indices]}:<br><br>${y_values}`;  
""")
# 将回调附加到 Tap 工具
taptool.callback = callback

#。。。。。。。

show(row(column(div_top,plot_install_plan,range_slider),div))


屏幕截图 2024-03-27 160711.png

最后就是下拉框和图纸了。建立一个下拉框选择tag,再建立一个div用于显示图纸,再在图纸上加上超链接就行了。
下拉框的options从上一步中获取,修改上一步的callback。


from bokeh.models import  Select
# 创建一个 Div 元素用于显示被点击的数据
selected_display = Div(text="", width=400, height=50)

# 创建一个下拉框来选择数据
select = Select(title="选择数据:", options=[])

# 定义点击事件的 JavaScript 回调
callback = CustomJS(args=dict(div=div,source=bars.data_source,x_axis=plot_install_plan.xaxis[0],y_data=bars.data_source.data['z'],title = '安装计划',
                             select=select,), code="""

            // 获取绘图数据
            var data = source.data;
            // 获取x值
            var x = data['x'];
             var indices = cb_data.source.selected.indices;
            console.log("点击了索引:" + indices);
            var indices = cb_data.source.selected.indices;
            console.log("点击了索引:" + indices);
            var y_indices = [];
            for (var i=0; i<indices.length; i++) {
                y_indices.push(indices[i]);
            }
            var y_values = [];
            for (var i=0; i<indices.length; i++) {
                y_values.push(y_data[indices[i]]);
            }
            div.text =`${title}:<br><br>${x[indices]}:<br><br>${y_values}`;  

            select.options = y_values[0].split('<br>');
            data_display.text = y_values[0].split('<br>');
""")
# 将回调附加到 Tap 工具
taptool.callback = callback

# 将回调函数绑定到下拉框的'on_change'事件上
callback1 = CustomJS(args=dict(
                             select=select,  selected_display=selected_display), code="""

            var selected_value = select.value;
            selected_display.text = `<h2>资料清单</h2>
                                    <li><a href="D:/work/python/bokeh_keshihua/pic/${selected_value}" target="_blank">${selected_value}资料文件夹</a></li>
                                    <li><a href="D:/work/python/bokeh_keshihua/pic/${selected_value}/${selected_value}总图.pdf" target="_blank">${selected_value}总图</a></li>
                                     <li><a href="D:/work/python/bokeh_keshihua/pic/${selected_value}/${selected_value}布置图.pdf" target="_blank">${selected_value}布置图</a></li>
                                     <li><a href="D:/work/python/bokeh_keshihua/pic/${selected_value}/${selected_value}详图.pdf" target="_blank">${selected_value}详图</a></li>
                                     <li><a href="D:/work/python/bokeh_keshihua/pic/${selected_value}/${selected_value}底座图纸.pdf" target="_blank">${selected_value}底座图纸</a></li>`; 
""")

select.js_on_change('value', callback1)

# customize the appearance of the grid and x-axis
from bokeh.models import NumeralTickFormatter
from bokeh.plotting import show
plot_install_plan.xgrid.grid_line_color = None
plot_install_plan.yaxis.formatter = NumeralTickFormatter(format="0,0")
plot_install_plan.xaxis.major_label_orientation = 0.8  # rotate labels by roughly pi/4

show(row(column(div_top,row(column(plot_install_plan,range_slider),div,column(select,selected_display)))))


屏幕截图 2024-03-27 162214.png

如果你想保持成html文件,那就在show前加一个output_file("responsive_plot.html")

全部代码如下(不包含数据模拟):

import pandas as pd

# 读取电子表格中的Sheet1数据
df = pd.read_excel('valve_schedule.xlsx', sheet_name='Sheet1',engine = 'calamine')

# 打印Sheet1数据的前几行
print(df.head())
#按日期统计数量
df['VIP'] = pd.to_datetime(df['VIP'])
max_date = df['VIP'].max()
min_date = df['VIP'].min()
df['VIP'] = df['VIP'].dt.strftime('%Y-%m-%d')
days_total = (max_date-min_date).days
#按日期统计数量
date_quantity_install = df.groupby('VIP')['Qty'].agg(sum).reset_index()
x_date_install = list(date_quantity_install.VIP.array)
y_quantiy_install = list(date_quantity_install.Qty.array)

#按日期统计Tag号
date_tags = df.groupby('VIP')['Tag'].agg(lambda x: '<br>'.join(x)).reset_index()
z_tag_install = list(date_tags['Tag'])

from bokeh.models import ColumnDataSource,NumeralTickFormatter, Div, RangeSlider, CustomJS, Div, Select
from bokeh.plotting import figure
from bokeh.layouts import column, row

source_install = ColumnDataSource(data = {'x': x_date_install,
                                  'y': y_quantiy_install,
                                 'z':z_tag_install,}
                         )
TOOLTIPS_install = [
    ("日期", " @x"),
    ("数量", " @y"),
    ("位号", " @z{safe}"),]
# 创建柱状图
plot_install_plan = figure( x_range=source_install.data["x"],
                                title="安装计划",
                                #width = 1000,
                                height=350,
                                tooltips=TOOLTIPS_install,
                               toolbar_location="above",

                            )
# 添加柱状图
bars = plot_install_plan.vbar( 
                        x="x",
                        top="y",
                        source=source_install,
                        #legend_label="安装计划",
                        width=0.6,
                                        )

# 在柱状图上添加文本标签
plot_install_plan.text(x='x', y='y', text='y', text_align='center', text_color = 'red',
       text_baseline='bottom', source=source_install)

div_top = Div(text="""<h3>某某项目: </h3><a href="https://en.wikipedia.org/wiki/HTML">项目名称</a>位于某某地方,计划投资某某亿,工期一年。。。。
<li>项目工期:2024年1月-2024年10月</li>  <li>项目地址:某某市某某区</li> <li>质量第一,安全第一!</li>""",
align = 'start',height=120)

#增加一个拖动条
range_slider = RangeSlider(
    title="Adjust x-axis range",
    start=1,
    end=(max_date-min_date).days,
    step=1,
    value=(35, 45),
)
range_slider.js_link("value", plot_install_plan.x_range, "start", attr_selector=0)
range_slider.js_link("value", plot_install_plan.x_range, "end", attr_selector=1)

from bokeh.models import CustomJS, Div, TapTool

# 添加 Tap 工具
taptool = TapTool()
plot_install_plan.add_tools(taptool)
#创建一个div
div = Div(width=150)

from bokeh.models import  Select
# 创建一个 Div 元素用于显示被点击的数据
selected_display = Div(text="", width=400, height=50)

# 创建一个下拉框来选择数据
select = Select(title="选择数据:", options=[])

# 定义点击事件的 JavaScript 回调
callback = CustomJS(args=dict(div=div,source=bars.data_source,x_axis=plot_install_plan.xaxis[0],y_data=bars.data_source.data['z'],title = '安装计划',
                             select=select,), code="""

            // 获取绘图数据
            var data = source.data;
            // 获取x值
            var x = data['x'];
             var indices = cb_data.source.selected.indices;
            console.log("点击了索引:" + indices);
            var indices = cb_data.source.selected.indices;
            console.log("点击了索引:" + indices);
            var y_indices = [];
            for (var i=0; i<indices.length; i++) {
                y_indices.push(indices[i]);
            }
            var y_values = [];
            for (var i=0; i<indices.length; i++) {
                y_values.push(y_data[indices[i]]);
            }
            div.text =`${title}:<br><br>${x[indices]}:<br><br>${y_values}`;  

            select.options = y_values[0].split('<br>');
            data_display.text = y_values[0].split('<br>');
""")
# 将回调附加到 Tap 工具
taptool.callback = callback

# 将回调函数绑定到下拉框的'on_change'事件上
callback1 = CustomJS(args=dict(
                             select=select,  selected_display=selected_display), code="""

            var selected_value = select.value;
            selected_display.text = `<h2>资料清单</h2>
                                    <li><a href="D:/work/python/bokeh_keshihua/pic/${selected_value}" target="_blank">${selected_value}资料文件夹</a></li>
                                    <li><a href="D:/work/python/bokeh_keshihua/pic/${selected_value}/${selected_value}总图.pdf" target="_blank">${selected_value}总图</a></li>
                                     <li><a href="D:/work/python/bokeh_keshihua/pic/${selected_value}/${selected_value}布置图.pdf" target="_blank">${selected_value}布置图</a></li>
                                     <li><a href="D:/work/python/bokeh_keshihua/pic/${selected_value}/${selected_value}详图.pdf" target="_blank">${selected_value}详图</a></li>
                                     <li><a href="D:/work/python/bokeh_keshihua/pic/${selected_value}/${selected_value}底座图纸.pdf" target="_blank">${selected_value}底座图纸</a></li>`; 
""")

select.js_on_change('value', callback1)

# customize the appearance of the grid and x-axis
from bokeh.models import NumeralTickFormatter
from bokeh.plotting import show
plot_install_plan.xgrid.grid_line_color = None
plot_install_plan.yaxis.formatter = NumeralTickFormatter(format="0,0")
plot_install_plan.xaxis.major_label_orientation = 0.8  # rotate labels by roughly pi/4

#output_file("responsive_plot.html")
show(row(column(div_top,row(column(plot_install_plan,range_slider),div,column(select,selected_display)))))

免费评分

参与人数 2威望 +1 吾爱币 +21 热心值 +2 收起 理由
爱飞的猫 + 1 + 20 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
hanshi + 1 + 1 谢谢@Thanks!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

头像被屏蔽
易一辅助 发表于 2024-3-27 17:01
提示: 作者被禁止或删除 内容自动屏蔽
heheluo 发表于 2024-3-27 21:28
1e3e 发表于 2024-3-28 08:19
CNLibrary 发表于 2024-3-28 09:53
好像很厉害的样子
 楼主| lianxiang1122 发表于 2024-3-28 10:03
简单的补充2点。首先是pandas,处理数据贼拉好用,但是呢太复杂了学习成本有点高,我不经常用就忘怎么用了,以前都是用openpyxl或xlwings读取表格自己写代码处理。。。。不仅慢不说还蛮烦。后来有了AI,那就爽到起飞了,告诉AI你表格的第一列第二列。。。。是哈,再告诉AI你的需求,让他用pandas处理,就不用自己记那么多命令了。calamine确实只支持新版pandas。。。。。
其次是CustomJS,根据命名就能看出来时客户JS,JS就是JavaScript,可以理解成像Python一样的编程语言,Python运行需要Python解析器,而JavaScript可以直接被浏览器识别。如果仅仅会Python这个CustomJS还真不好写。建议你去B站花点时间去入入门。
CustomJS(args=dict(), code="""  """)里面我们就放了2个参数,一个args,dict里面可以放很多参数,例如elect=select,等号前是一个自定义变量,你可以任意命名,用于后面code里引用,等号后面就是Python前面那些数据,例如select=select,前面的select是自定义变量,可以修改,例如修改为s,后面的select就是前面代码里的数据, 当后面的code引用时,就用等号前面的变量名s。code就是我们自己写的一些js了,最后bokeh就会一起加上去,最终生成一个html文件。
mytomsummer 发表于 2024-4-1 09:44
感谢楼主分享资源
locoman 发表于 2024-8-4 11:09
楼主666!谢谢无私分享,收藏学习。
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-11-24 12:29

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表