lianxiang1122 发表于 2024-3-27 16:33

bokeh库实现项目计划可视化

bokeh库是一个十分强大的可视化库,生成html文件实现互动式可视化。
模拟一个简单的阀门安装计划。



首先,利用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()

# 创建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文件,创建出一些模拟图纸),如下:


```
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就可以进入学习界面了。


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

首先,用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还差一些。。。


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

首先,上方的文本好解决,加一个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))
```



再在下方加一个拖动条,需要用到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))
```



最麻烦的就是左侧显示了,显示是根据鼠标所在的触发位置信息显示,这就需要用到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,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);
            }
            var y_values = [];
            for (var i=0; i<indices.length; i++) {
                y_values.push(y_data]);
            }
            div.text =`${title}:<br><br>${x}:<br><br>${y_values}`;
""")
# 将回调附加到 Tap 工具
taptool.callback = callback

#。。。。。。。

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

```



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

```

from bokeh.models importSelect
# 创建一个 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,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);
            }
            var y_values = [];
            for (var i=0; i<indices.length; i++) {
                y_values.push(y_data]);
            }
            div.text =`${title}:<br><br>${x}:<br><br>${y_values}`;
            
            select.options = y_values.split('<br>');
            data_display.text = y_values.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)))))
```



如果你想保持成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 importSelect
# 创建一个 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,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);
            }
            var y_values = [];
            for (var i=0; i<indices.length; i++) {
                y_values.push(y_data]);
            }
            div.text =`${title}:<br><br>${x}:<br><br>${y_values}`;
            
            select.options = y_values.split('<br>');
            data_display.text = y_values.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)))))


```

易一辅助 发表于 2024-3-27 17:01

heheluo 发表于 2024-3-27 21:28

可以呀,楼主给的这个技术贴不错

1e3e 发表于 2024-3-28 08:19

calamine不支持pandas1.3.5

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!谢谢无私分享,收藏学习。
页: [1]
查看完整版本: bokeh库实现项目计划可视化