Pyecharts使用JScode回调函数时遇到的函数内换行符失效问题

Pyecharts使用JScode回调函数时遇到的函数内换行符失效问题,第1张

前记:这个准确来说不是问题,是bug……我愿意把它评为我今年遇到的最折磨人的bug;这个bug很少有人能踩坑,原因主要是bug触发的条件比较苛刻(看题目一长串就知道了):
  1. 需要在pyecharts中使用JScode(一般来说,当label、tooltips里面想要显示一些特殊的内容时,而formatter的字符串模板形式并不支持时,只能用jscode来实现)
  2. JScode中返回的字符串有换行需求,即插入换行符“\n”
  3. 使用pyecharts时习惯先创建一个pyecharts对象,然后再通过对象调用渲染系列方法(render(),render_notebook(),render_embed())得到图像,如下风格:
    from pyecharts.charts import Bar
    
    bar = (
        Bar()
        .add_xaxis(["衬衫", "羊毛衫", "雪纺衫", "裤子", "高跟鞋", "袜子"])
        .add_yaxis("商家A", [5, 20, 36, 10, 75, 90])
    )
    bar.render()

    而不是用这种直接渲染,不使用变量指向该对象的方法:
     Bar() .add_xaxis(["衬衫", "羊毛衫", "雪纺衫", "裤子", "高跟鞋", "袜子"])
        .add_yaxis("商家A", [5, 20, 36, 10, 75, 90]) 
        .render()
  4. 对创建的pyecharts对象使用了任意两次渲染方法,即:
    from pyecharts.charts import Bar
    
    bar = (
        Bar()
        ……
    )
    
    bar.render()
    bar.render_notebook()

最后来把这四点写在一起来个bug暴雷示例,就是:

from pyecharts.charts import Bar
from pyecharts.commons.utils import JsCode

bar = (
    Bar()
    .add_xaxis(
    ……
    label_opts=opts.LabelOpts(
    ……
        formatter=JsCode("function(x){……return '\n'……}"),
    ……
)
bar.render()
bar.render_notebook()

真实情况远比这个复杂得多,因为一个pyecharts对象里面还要配置其它项目,当bug第一次发生的时候,很难第一时间就排除代码的其它部分,直接想到bug的源头是在这里

 1 实际出bug的代码介绍

业务需求:画“不同类型客户的数量”的树状图时,希望树状图的标签和悬浮提示框中不仅显示客户类型的名称、该类客户数量,还需要显示该类客户占总体的比例,然而pyecharts中,只对少数图形(例如:饼状图、仪表图、漏斗图可以直接用{d}来自动表示比例)支持自动计算各项比例,其它图形只能靠自己手动实现,因此,我盯上了JScode……

from pyecharts import options as opts
from pyecharts.charts import TreeMap
from pyecharts.commons.utils import JsCode

c = (TreeMap()
    .add("RFM各客户价值类型人数", data,
         tooltip_opts=opts.TooltipOpts(
             formatter=
                 JsCode(
                 """
                 function(x){
                 return
                 x.data.name +':'+x.data.value+'人

(' +(Number(x.data.value)/Number(x.treePathInfo[0].value)*100).toFixed(2)+ '%)';} """ ), textstyle_opts=opts.TextStyleOpts( align="center" ) ), label_opts=opts.LabelOpts( formatter=JsCode( "function(x){return x.data.name+'\n'+x.data.value+'人 ('+(Number(x.data.value)/Number(x.treePathInfo[0].value)*100).toFixed(2)+ '%)';}" ), position="inside" ), ) .set_global_opts( toolbox_opts=opts.ToolboxOpts( feature=opts.ToolBoxFeatureOpts( data_zoom=None, brush=None, magic_type=None, ) ) ) ) c.render() c.render_notebook()

 注意本行代码

formatter=JsCode
(
"function(x){return x.data.name+'\n'+x.data.value+'人 ('+Number(x.data.value)/Number(x.treePathInfo[0].value)*100).toFixed(2)+ '%)';}"
)

希望返回的数据格式应该是在中间换行

一般保持客户

 1246人(%2.98)

但现实猛然打脸

2 漫漫排bug之路 2.1 寻找换行符\n

看到这个现象,第一反应就是“忘了加换行符\n”,但是瞪大了我的眼睛确认了好几遍,也还是确定我在正确的位置加了换行符

 2.2 确定换行符格式和可行性

因为JScode本质上是写一串JavaScript代码,pyecharts不会使用python内核对这行代码执行,只是简单处理后交给JavaScript来执行,那么会不会是因为这个过程,导致换行符有特殊要求,我没有遵循这个要求从而失效了?

带着这个猜想查找官方文档,首先确认了,JScode是支持换行的

 

然后在官方文档继续查找,看到了这句话

 

“柳暗花明又一村”!(不得不说有点坑哈,在api那里说可以直接用\n换行,在这里才说需要用\\n换行)

满怀欣喜地把对应代码改成,然后准备迎接排除bug的喜悦

formatter=JsCode
(
"function(x){return x.data.name+'\n'+x.data.value+'人 ('+Number(x.data.value)/Number(x.treePathInfo[0].value)*100).toFixed(2)+ '%)';}"
)

然而……

没错,问题照旧(所以这里的图都懒得重新截图,还是上面那张)

【小小剧透,其实一般人到这里应该已经完美解决了,但是某个隐藏的bug仍然阻止了换行,所以当时的我丝毫没有意识到这点;这波是bug上加bug】

2.3 试图寻找问题的根源

上面说过,pyecharts其实是基于echarts开发的,使用echarts的原理是,echarts首先用js开发了一个自己的绘图库,用户只需要按照js库的功能接口规范编写好调用代码,交给echarts.js执行即可,echarts.js会把绘图渲染的结果渲染到浏览器上。而pyecharts做的事情就是把用户写的pyecharts代码封装成对应的js代码。

那么,我在pyecharts里面写好的代码,是不是在转换到echarts的时候变了样呢?【剧透:是的】

于是,我用pycharm打开了这个代码(之前是在jupyter notebook里面写的,因为方便看到实时渲染结果),然后运行代码后查看变量——生成的pyecharts对象,这一看还真看出了端倪:

  • 首先找到变量c,即我创建的这个pyecharts对象
  • 然后查看c的属性,找到由上面的代码生成的JScode

    (有点难找,在c-options-series-0-label-opts-formatter-js_code里面)
  • 仔细一看还真看出了问题,我在pyecharts代码里面写的"\\n"在运行后,生成的对象里面变成了""空字符串?
    (严格来说,图中显示的是\"\",因为要对引号转义,这里方便叙述,就没有带上转义符号)
 2.4-2.9 ……令人抓狂的试错

虽然知道了代码运行后,生成的pyecharts对象里面会莫名把我写的换行符给“吃掉”,但如何避免这个情况,博主在这过程中一直一筹莫展,查了某度,试了各种玄学方法,例如

用Unicode编码代替原代码中的\n,写成

'\u000A'

在原代码的字符串前面加上r(表示该字符串内所有的转义都被禁止),写成

r"function(x){return x.data.name+'\\n'+x.data.value+'人 ('+Number(x.data.value)/Number(x.treePathInfo[0].value)*100).toFixed(2)+ '%)';}"

换个python内核版本再运行一次;换个IDE再运行一次(是不是有点愚蠢,但是人逼急了啥玄学方法都愿意尝试)

 改jupyter notebook的语言环境,避免使用中文

尝试render_embed()方法看渲染后的结果

(本来这个方法也无限接近真相了,但是我还是脑子一抽把render_embed()和render()一起用了,和真相擦肩而过) 

 播放《好运来》,拜一拜python之父

…… 

 都毫无作用

3.3 成也凑巧,败也凑巧

在被这个bug折磨了3小时后,博主无意中发现,虽然jupyter notebook里面实时渲染的图形一直显示有问题,但是博主还使用render()方法生成了render.html啊,那么打开这个html文件会不会有啥不同呢?

 ohhhhhhh!

bug终于消失了,但是为什么呢?难道是render_notebook()这个渲染方法单独出了bug吗?

在经历一系列走弯路后(博主又花了1.5小时,主要原因是先入为主地认为问题出在具体的方法上面,没有考虑到别的地方,然后显而易见地走错了方向,一直没有收获;但是博主一开始并没有觉得这个debug失败是思考方向有问题所致,反而觉得这个bug是偶发性bug)

后来,又是一次偶然,发现问题出在对同一个pyecharts对象渲染了两次的时候,意思是说

正常换行的情况:
 

c=(Treemap()

……

)

c.render()

这样是ok的

或者

c=(Treemap()

……

)

c.render_notebook()

这样也ok

没有正常换行的情况:

c=(Treemap()

……

)
c.render()
c.render_notebook()

render()生成的html文件里面的图形换行了,但是c.render_notebook()在jupyter notebook里面生成的图形没有换行

如果是这样:

c=(Treemap()

……

)

c.render_notebook()

c.render()

则恰好相反,jupyter notebook里面生成的图形是正常换行的,但render()生成的html文件里面的图形却没有换行了

 

 

结论:使用JScode时,生成的Pyecharts对象被任意render类的方法渲染一次后,其内部变量JScode字符就会变化,失去换行符\n,从而当其再次被渲染时,渲染出来的图形对应文字部分不再换行

说这次debug过程是“成也凑齐,败也凑巧”,是因为这个bug出发条件实际上很严苛,触发它本身就是凑巧;而“成也凑巧”是因为发现这个bug真正的原因也是凑巧发现的——我因为找bug一无所成,一起之下砸了下键盘,不小心碰到了“D”键,删除了原来的代码,还关了网页和kernel……;然后我赶紧凭着记忆写了一份,在末尾无意中调换了c.render()和c.render_notebook()的顺序,这才观察到上面的这个现象,才敢确定真正的bug源头在于渲染的顺序和次数。

由于官方没有任何文档说到这个特性,而且从pyecharts的设计思路来看,pyecharts绘图对象应该是可以重复渲染图形而本身而不受任何影响的,所以基本认定这个现象为bug

后记:大家对如何写JScode来实现自定义功能感兴趣吗?虽然这次的bug就是JScode引出的,但是遇到pyecharts自带的设置不能满足绘图要求时,我还是觉得JScode真香!

欢迎分享,转载请注明来源:内存溢出

原文地址: http://www.outofmemory.cn/langs/719163.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-25
下一篇 2022-04-25

发表评论

登录后才能评论

评论列表(0条)

保存