hyperquant 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hyperquant/__init__.py +5 -0
- hyperquant/core.py +456 -0
- hyperquant/draw.py +1199 -0
- hyperquant/logkit.py +190 -0
- hyperquant-0.1.0.dist-info/METADATA +153 -0
- hyperquant-0.1.0.dist-info/RECORD +7 -0
- hyperquant-0.1.0.dist-info/WHEEL +4 -0
hyperquant/draw.py
ADDED
@@ -0,0 +1,1199 @@
|
|
1
|
+
import pathlib
|
2
|
+
import uuid
|
3
|
+
import pandas as pd
|
4
|
+
import os
|
5
|
+
from pyecharts.charts import Line, Grid, Kline, Bar
|
6
|
+
from pyecharts import options as opts
|
7
|
+
from pyecharts.commons.utils import JsCode
|
8
|
+
import webbrowser
|
9
|
+
|
10
|
+
top_height = 45 # 图表最上面标题区高度
|
11
|
+
h_space = 3 # 每个曲线图之间的间隔
|
12
|
+
left_data_div_width = 400 # 左侧数据显示区宽度
|
13
|
+
|
14
|
+
|
15
|
+
# left_tl_width=100 # 左侧图例
|
16
|
+
|
17
|
+
|
18
|
+
def draw(
|
19
|
+
df: pd.DataFrame,
|
20
|
+
data_dict: list,
|
21
|
+
date_col: str,
|
22
|
+
date_formate: str = "%Y-%m-%d %H:%M:%S",
|
23
|
+
pic_width: int = 1500,
|
24
|
+
title: str = "数据查看",
|
25
|
+
path: str = None,
|
26
|
+
show: bool = True,
|
27
|
+
display_js=None,
|
28
|
+
auto_play_space="",
|
29
|
+
play_ibe_js="",
|
30
|
+
height_type: str = "px",
|
31
|
+
zoom_space=100,
|
32
|
+
zoom_end=None,
|
33
|
+
right_width=300,
|
34
|
+
right_data_view=True,
|
35
|
+
):
|
36
|
+
"""
|
37
|
+
:param df: 必填,包含净值的数据字典格式
|
38
|
+
:param data_dict: 必填,要显示的每个图表的配置,具体配置中的属性如下:[{
|
39
|
+
df:df, # 不必填,如果不填,表示本图表的数据为入参df,如果本属性有值表示本图表的数据在另一个df中
|
40
|
+
series_name: 不必填,因子显示名称,如果为空,则跟col一样。
|
41
|
+
col: 不必填,df中的列名,如果是K线则为["open", "close", "lowest", "highest"]的各列名,如果为穿鞋,则跟series_name一样
|
42
|
+
draw_type:"Kline", 默认Line,图表格式,有Kline(K线),Line(曲线),Bar(柱状图),DownLine(回撤图)
|
43
|
+
can_change_log:False, 默认False,是否可以转换对数值显示
|
44
|
+
height:200, 默认200,图表高度
|
45
|
+
is_smooth:True, 默认为True,曲线是否平滑显示,本参数只对曲线有效
|
46
|
+
is_symbol_show:False, 默认为False,曲线或回撤图是否显示点,本参数只对曲线和回撤图有效
|
47
|
+
check_col, 不必填,如果是Bar有可能显示的是成交量成交额之类的,需要当前涨显示红色,跌显示绿色,本属性是表示df中哪一列表示涨跌幅,或可以用来判断柱状图颜色
|
48
|
+
color: 不必填,线的颜色,默认自动分配颜色
|
49
|
+
window_cumprod_base_one: 不必填,默认为False。可见窗口里是否从1开始算净值,本配置只对曲线生效,一般用在原曲线数据是涨跌幅,并且在可见的窗口里总是从1开始累计净值的情况
|
50
|
+
split_color_col: 不必填,默认为None。与color属性冲突。轮动资金曲线或净值曲线需要根据不同的子策略分段显示不同的颜色时使用,本属性表示用df中的哪个列判断子策略。上面是业务角度的描述,通用的描述就是曲线需要根据df中的某列显示不同的颜色场景。
|
51
|
+
split_color: 不必填,默认为None。与color属性冲突。如果split_color_col有值,则本参数一定要配置。当split_color_col有值时,可以配置本属性,用来描述每个子策略对应的颜色
|
52
|
+
trade_single: 不必填,默认为None,买入卖出信号列,如果该属性配置了,则图上会根据该列的值如果为1显示做多,如果为-1显示做空,0表示平仓
|
53
|
+
kline_color_type: 不必填,默认为True,表示K线的颜色方式,True为涨红跌绿,False为涨绿跌红
|
54
|
+
dec_length:不必填,默认为None,表示显示本曲线的数据时显示小数的位数,None为不控制
|
55
|
+
},...]
|
56
|
+
:param date_col: 必填,时间列的名字,如果为None将用索引作为时间列
|
57
|
+
:param date_formate: 不必填,默认为%Y-%m-%d %H:%M:%S "年-月-日 时:分:秒"时间列显示格式
|
58
|
+
:param pic_width: 不必填,默认为1500,图表的宽度,注:图表的高度会根据data_dict中的每个图表的高度自动算出
|
59
|
+
:param title: 不必填,默认为“数据查看”,整体表图标题
|
60
|
+
:param path: 不必填,默认为当前代码所在目录的chart.html,图片路径
|
61
|
+
:param show: 不必填,默认为True,是否打开图片
|
62
|
+
:param display_js: 不必填,默认为None,右侧扩展信息区显示处理,js内容中必须包括function set_ext_data_div(ext_data_div,zoom_end,zoom_space)函数
|
63
|
+
:param auto_play_space: 不必填,自动播放间隔时间计算函数,必须是js函数,js中必须包括function auto_play_space(xi)函数,返回值的单位为毫秒
|
64
|
+
:param play_ibe_js: 不必填,跳转或自动播放时,需要自动跳一下跳的下一个时间范围,比如整个时间范围一次移动一个持股周期,必须是js函数,js中必须包括play_ibe_js(ibe,next),ibe当前区间begin和end,next为true表示周期范围向后跳,next为false表示 周期范围向前跳
|
65
|
+
:param height_type: 子图的高度数据类型,有%和px两种,如果值为%表示data_dict中子图的height值代表百分比,否则代表px
|
66
|
+
:param zoom_space: 不必填,默认值 20,初始X轴窗口显示日期周期数
|
67
|
+
:param zoom_end: 不必填,默认值 None,X轴窗口默认结束点
|
68
|
+
:param right_width: 不必填,默认值 300,右侧信息显示区的宽度
|
69
|
+
:param right_data_view:不必填,默认为True,表示右侧信息显示区是否显示曲线的数据
|
70
|
+
"""
|
71
|
+
draw_df = df
|
72
|
+
time_data = _produce_x_date(draw_df, date_col, date_formate) # 计量时间轴
|
73
|
+
global left_data_div_width
|
74
|
+
left_data_div_width = right_width # 右侧数据显示区宽度
|
75
|
+
# left_all_width = left_data_div_width # 图表左侧区域宽度,如果扩展区别不显示内容,则为数据显示区的1倍,否则为2倍
|
76
|
+
# if display_js is not None:
|
77
|
+
# left_all_width = left_all_width * 2
|
78
|
+
# 设置入参的默认值,同时计算整个图表的高度
|
79
|
+
# pre_height = 100 / (len(data_dict) + 1)
|
80
|
+
# 设置data_dict中的默认值,计算出的整个图表的高度all_h(只在height_type为px时all_h值才有意义),同时如果所有子图都未设置高度height_type会被强制设成%
|
81
|
+
data_dict, all_h, height_type = _set_default_value(data_dict, height_type)
|
82
|
+
if height_type == "%":
|
83
|
+
grid_height = "100%"
|
84
|
+
else:
|
85
|
+
grid_height = f"{all_h}px"
|
86
|
+
grid_chart = Grid(
|
87
|
+
init_opts=opts.InitOpts(
|
88
|
+
width=f"100%",
|
89
|
+
height=grid_height,
|
90
|
+
animation_opts=opts.AnimationOpts(
|
91
|
+
animation=True, animation_easing="linear"
|
92
|
+
),
|
93
|
+
page_title=title,
|
94
|
+
# renderer=RenderType.SVG,
|
95
|
+
),
|
96
|
+
)
|
97
|
+
i = 0
|
98
|
+
js_code = "" # 额外配置参数、对数显示转换等html和javascript脚本
|
99
|
+
js_data_item = "var line_other_param=["
|
100
|
+
if height_type == "%":
|
101
|
+
cur_top = 0
|
102
|
+
else:
|
103
|
+
cur_top = top_height # 当前空白区域的最上面纵坐标
|
104
|
+
# 循环处理每个图表的显示参数
|
105
|
+
visual_map_all_js = ""
|
106
|
+
mark_point_js_all = ""
|
107
|
+
ii = 0
|
108
|
+
for data_item in data_dict:
|
109
|
+
# 把图表加到grid中
|
110
|
+
line, sub_js_code, visual_map_js, mark_point_js = _produce_one_chart(
|
111
|
+
grid_chart,
|
112
|
+
draw_df,
|
113
|
+
data_item,
|
114
|
+
time_data,
|
115
|
+
cur_top,
|
116
|
+
title,
|
117
|
+
len(data_dict),
|
118
|
+
i,
|
119
|
+
pic_width,
|
120
|
+
height_type,
|
121
|
+
data_dict,
|
122
|
+
ii,
|
123
|
+
df,
|
124
|
+
date_col,
|
125
|
+
) # 创建每个图表的配置信息
|
126
|
+
js_data_item += sub_js_code
|
127
|
+
visual_map_all_js += visual_map_js
|
128
|
+
mark_point_js_all += mark_point_js
|
129
|
+
if "sub_chart" in data_item:
|
130
|
+
for sub_data_item in data_item["sub_chart"]:
|
131
|
+
sub_line, sub_js_code, visual_map_js, mark_point_js = (
|
132
|
+
_produce_one_chart(
|
133
|
+
grid_chart,
|
134
|
+
draw_df,
|
135
|
+
sub_data_item,
|
136
|
+
time_data,
|
137
|
+
cur_top,
|
138
|
+
title,
|
139
|
+
1,
|
140
|
+
1,
|
141
|
+
pic_width,
|
142
|
+
height_type,
|
143
|
+
data_dict,
|
144
|
+
ii,
|
145
|
+
df,
|
146
|
+
date_col,
|
147
|
+
)
|
148
|
+
)
|
149
|
+
ii += 1
|
150
|
+
js_data_item += sub_js_code
|
151
|
+
visual_map_all_js += visual_map_js
|
152
|
+
mark_point_js_all += mark_point_js
|
153
|
+
line.overlap(sub_line)
|
154
|
+
if height_type == "%":
|
155
|
+
pos_bottom = ""
|
156
|
+
if i == 0:
|
157
|
+
pos_bottom = f"{100 - data_item['height']}%"
|
158
|
+
if i == len(data_dict) - 1:
|
159
|
+
pos_bottom = "80"
|
160
|
+
if i == 0:
|
161
|
+
grid_chart.add(
|
162
|
+
line,
|
163
|
+
grid_opts=opts.GridOpts(
|
164
|
+
pos_left="45",
|
165
|
+
pos_right=f"{left_data_div_width + 80}",
|
166
|
+
pos_top=top_height,
|
167
|
+
pos_bottom=pos_bottom,
|
168
|
+
),
|
169
|
+
)
|
170
|
+
elif i == len(data_dict) - 1:
|
171
|
+
grid_chart.add(
|
172
|
+
line,
|
173
|
+
grid_opts=opts.GridOpts(
|
174
|
+
pos_left="45",
|
175
|
+
pos_right=f"{left_data_div_width + 80}",
|
176
|
+
pos_top=f"{cur_top}%",
|
177
|
+
pos_bottom=pos_bottom,
|
178
|
+
),
|
179
|
+
)
|
180
|
+
else:
|
181
|
+
grid_chart.add(
|
182
|
+
line,
|
183
|
+
grid_opts=opts.GridOpts(
|
184
|
+
pos_left="45",
|
185
|
+
pos_right=f"{left_data_div_width + 80}",
|
186
|
+
pos_top=f"{cur_top}%",
|
187
|
+
height=f"{data_item['height']}%",
|
188
|
+
),
|
189
|
+
)
|
190
|
+
else:
|
191
|
+
if i == 0:
|
192
|
+
grid_chart.add(
|
193
|
+
line,
|
194
|
+
grid_opts=opts.GridOpts(
|
195
|
+
pos_left="45",
|
196
|
+
pos_right=f"{left_data_div_width + 80}",
|
197
|
+
pos_top=cur_top,
|
198
|
+
height=data_item["height"],
|
199
|
+
),
|
200
|
+
)
|
201
|
+
elif i == len(data_dict) - 1:
|
202
|
+
grid_chart.add(
|
203
|
+
line,
|
204
|
+
grid_opts=opts.GridOpts(
|
205
|
+
pos_left="45",
|
206
|
+
pos_right=f"{left_data_div_width + 80}",
|
207
|
+
pos_top=cur_top,
|
208
|
+
height=data_item["height"],
|
209
|
+
),
|
210
|
+
)
|
211
|
+
else:
|
212
|
+
grid_chart.add(
|
213
|
+
line,
|
214
|
+
grid_opts=opts.GridOpts(
|
215
|
+
pos_left="45",
|
216
|
+
pos_right=f"{left_data_div_width + 80}",
|
217
|
+
pos_top=cur_top,
|
218
|
+
height=data_item["height"],
|
219
|
+
),
|
220
|
+
)
|
221
|
+
# 曲线类型的图表 加上切换 对数显示 的相关脚本
|
222
|
+
if data_item["draw_type"] == "Line" or data_item["draw_type"] == "Bar":
|
223
|
+
js_code = js_code + _produce_change_log_js(
|
224
|
+
data_item, cur_top, i, height_type
|
225
|
+
)
|
226
|
+
cur_top = cur_top + data_item["height"] # 更新当前空白区域的最上面top
|
227
|
+
i = i + 1
|
228
|
+
ii += 1
|
229
|
+
js_code += f"""
|
230
|
+
<script>
|
231
|
+
function fixDecLen(fd,dec_length){{
|
232
|
+
//line_other_param[i].dec_length
|
233
|
+
if (dec_length>=0){{
|
234
|
+
let dzz=10 ** dec_length;
|
235
|
+
fd=Math.round(fd * dzz) / dzz;
|
236
|
+
//fd=fd.toFixed(dec_length);
|
237
|
+
}}
|
238
|
+
return fd;
|
239
|
+
}}
|
240
|
+
{js_data_item}
|
241
|
+
];
|
242
|
+
var cc=document.getElementsByClassName("chart-container");
|
243
|
+
var cc_name=cc[0].id;
|
244
|
+
var chart_ins=window["chart_"+cc_name];
|
245
|
+
var dataZoom_startValue_i=0;
|
246
|
+
var dataZoom_endValue_i=0;
|
247
|
+
var dataZoom_startValue=0;
|
248
|
+
var dataZoom_endValue=0;
|
249
|
+
var chart_option=window["option_"+cc_name];
|
250
|
+
var chart_option_back=JSON.parse(JSON.stringify(chart_option));
|
251
|
+
"""
|
252
|
+
if len(visual_map_all_js) > 0:
|
253
|
+
js_code += f"""
|
254
|
+
chart_option["visualMap"]=[
|
255
|
+
{visual_map_all_js}
|
256
|
+
];
|
257
|
+
"""
|
258
|
+
if len(mark_point_js_all) > 0:
|
259
|
+
js_code += mark_point_js_all
|
260
|
+
js_code += _produce_tooltip_formatter()
|
261
|
+
# if len(visual_map_all_js) + len(mark_point_js_all) > 0:
|
262
|
+
js_code += """
|
263
|
+
chart_ins.setOption(chart_option);
|
264
|
+
"""
|
265
|
+
if display_js is not None:
|
266
|
+
js_code += f"""
|
267
|
+
{display_js}
|
268
|
+
"""
|
269
|
+
if auto_play_space is not None:
|
270
|
+
js_code += f"""
|
271
|
+
{auto_play_space}
|
272
|
+
"""
|
273
|
+
if play_ibe_js is not None:
|
274
|
+
js_code += f"""
|
275
|
+
{play_ibe_js}
|
276
|
+
"""
|
277
|
+
js_code += "</script>\n"
|
278
|
+
js_code = js_code + _produce_date_opt_js(
|
279
|
+
pic_width, display_js, height_type, title, zoom_space, zoom_end, right_data_view
|
280
|
+
) # 生成日期跳转相关的脚本
|
281
|
+
# 生成网页内容并写到html文件中
|
282
|
+
if path is None: # 默认写入到当前文件夹的chart.html中
|
283
|
+
path = os.path.abspath(os.path.dirname(__file__)) + "/chart.html"
|
284
|
+
with open(path, "w", encoding="utf-8") as f:
|
285
|
+
f.write(f"{grid_chart.render_embed()}{js_code}")
|
286
|
+
if show:
|
287
|
+
path = pathlib.Path(path).absolute().as_uri()
|
288
|
+
webbrowser.open(path)
|
289
|
+
|
290
|
+
|
291
|
+
def _set_default_value(data_dict, height_type):
|
292
|
+
# px和百分比两种指定高度的模式,不能混用,用一个入参控制。
|
293
|
+
# 1.用px指定高度:页面会向下变长,没有指定的子图高度默认200;
|
294
|
+
# 2.用 % 指定高度:所有子图在高度上刚好撑满屏幕,第一个图要让40px出来显示上面标题等内容;没有指定的子图高度用剩下的高度平分,
|
295
|
+
# 3.所有子图都未指定高度:不管height_type是什么类型,第一个图是其它所有图的双倍高,其它图一样高,用百分比,撑满屏幕。
|
296
|
+
all_h = top_height # 整个图表的高度
|
297
|
+
set_height = 0 # 配置了height的所有子图的高度汇总值
|
298
|
+
set_height_num = 0 # 配置了height的所有子图的个数
|
299
|
+
for data_item in data_dict:
|
300
|
+
if "height" in data_item:
|
301
|
+
set_height += data_item["height"]
|
302
|
+
set_height_num += 1
|
303
|
+
if set_height == 0:
|
304
|
+
height_type = "%"
|
305
|
+
def_height = int(100 / (len(data_dict) + 1))
|
306
|
+
else:
|
307
|
+
if height_type == "%" and (len(data_dict) - set_height_num) > 0:
|
308
|
+
def_height = int((100 - set_height) / (len(data_dict) - set_height_num))
|
309
|
+
else:
|
310
|
+
def_height = 200
|
311
|
+
i = 0
|
312
|
+
for data_item in data_dict:
|
313
|
+
if "height" not in data_item:
|
314
|
+
if i == 0 and set_height == 0:
|
315
|
+
data_item["height"] = def_height * 2
|
316
|
+
else:
|
317
|
+
data_item["height"] = def_height
|
318
|
+
_set_data_item_value(data_item)
|
319
|
+
if "sub_chart" in data_item:
|
320
|
+
for sub_data in data_item["sub_chart"]:
|
321
|
+
_set_data_item_value(sub_data)
|
322
|
+
all_h = all_h + data_item["height"] # + h_space
|
323
|
+
i += 1
|
324
|
+
all_h = all_h + 80 # - h_space # 在最后一个图表的后面加50,用来显示时间选择轴
|
325
|
+
return data_dict, all_h, height_type
|
326
|
+
|
327
|
+
|
328
|
+
def _set_data_item_value(data_item):
|
329
|
+
if "series_name" not in data_item:
|
330
|
+
if "col" in data_item:
|
331
|
+
data_item["series_name"] = data_item["col"]
|
332
|
+
else:
|
333
|
+
raise ValueError("数据显示项的配置中不能series_name和col都为空")
|
334
|
+
if "draw_type" not in data_item:
|
335
|
+
data_item["draw_type"] = "Line"
|
336
|
+
if "is_smooth" not in data_item:
|
337
|
+
data_item["is_smooth"] = True
|
338
|
+
if "is_symbol_show" not in data_item:
|
339
|
+
data_item["is_symbol_show"] = False
|
340
|
+
if "kline_color_type" not in data_item:
|
341
|
+
data_item["kline_color_type"] = True
|
342
|
+
if "dec_length" not in data_item:
|
343
|
+
data_item["dec_length"] = None
|
344
|
+
|
345
|
+
|
346
|
+
def _produce_one_chart(
|
347
|
+
grid_chart,
|
348
|
+
draw_df,
|
349
|
+
data_item,
|
350
|
+
time_data,
|
351
|
+
cur_top,
|
352
|
+
title,
|
353
|
+
chart_count,
|
354
|
+
i,
|
355
|
+
pic_width,
|
356
|
+
height_type,
|
357
|
+
data_dict,
|
358
|
+
ii,
|
359
|
+
main_df,
|
360
|
+
date_col,
|
361
|
+
):
|
362
|
+
"""
|
363
|
+
生成一个图表的绘图配置
|
364
|
+
:param grid_chart: 整合所有图表的grid组件
|
365
|
+
:param draw_df: 本图表使用的数据集
|
366
|
+
:param data_item: 本图表的入参配置
|
367
|
+
:param time_data: 图表时间轴数据
|
368
|
+
:param cur_top: 当前空白区域最上面的纵坐标
|
369
|
+
:param title: 图表title
|
370
|
+
:param chart_count: 总计图表数,主要用在生成第一个图表时配置项中的多表联动部分需要知道一共有多少个图表
|
371
|
+
:param i: 当前是第几个图表,主要用在判断是否是第一个图表,如果是第一个图表需要做多表联动相关的配置
|
372
|
+
"""
|
373
|
+
js_code = f"{{name:'{data_item['series_name']}',"
|
374
|
+
if "window_cumprod_base_one" in data_item and data_item["window_cumprod_base_one"]:
|
375
|
+
js_code += f"window_cumprod_base_one:true,"
|
376
|
+
else:
|
377
|
+
js_code += f"window_cumprod_base_one:false,"
|
378
|
+
if "color" in data_item:
|
379
|
+
js_code += f"color:'{data_item['color']}',"
|
380
|
+
if "dec_length" in data_item and data_item["dec_length"] is not None:
|
381
|
+
js_code += f"dec_length:{data_item['dec_length']},"
|
382
|
+
else:
|
383
|
+
js_code += f"dec_length:-1,"
|
384
|
+
js_code += "},"
|
385
|
+
if "df" in data_item:
|
386
|
+
draw_df = data_item["df"]
|
387
|
+
# 计算当前图表的显示数据列
|
388
|
+
if "col" in data_item: # 如果没有列名,则表示列名与显示名一致
|
389
|
+
col = data_item["col"]
|
390
|
+
else:
|
391
|
+
if data_item["draw_type"] == "Kline":
|
392
|
+
col = ["open", "close", "lowest", "highest"]
|
393
|
+
else:
|
394
|
+
col = data_item["series_name"]
|
395
|
+
if "draw_type" in data_item and data_item["draw_type"] == "Kline":
|
396
|
+
line = Kline()
|
397
|
+
elif "draw_type" in data_item and data_item["draw_type"] == "Bar":
|
398
|
+
line = Bar()
|
399
|
+
else:
|
400
|
+
line = Line()
|
401
|
+
line.add_xaxis(time_data) # 设置图表横轴为时间轴
|
402
|
+
if "color" in data_item:
|
403
|
+
linestyle_opts = opts.LineStyleOpts(color=data_item["color"])
|
404
|
+
itemstyle_opts = opts.ItemStyleOpts(color=data_item["color"])
|
405
|
+
else:
|
406
|
+
linestyle_opts = opts.LineStyleOpts()
|
407
|
+
itemstyle_opts = opts.ItemStyleOpts()
|
408
|
+
if data_item["draw_type"] == "Kline":
|
409
|
+
data_list = draw_df[col].to_numpy().tolist()
|
410
|
+
if not data_item["kline_color_type"]:
|
411
|
+
itemstyle_opts = opts.ItemStyleOpts(
|
412
|
+
color="#14b143",
|
413
|
+
color0="#ef232a",
|
414
|
+
border_color="#14b143",
|
415
|
+
border_color0="#ef232a",
|
416
|
+
)
|
417
|
+
else:
|
418
|
+
itemstyle_opts = opts.ItemStyleOpts(
|
419
|
+
color="#ef232a",
|
420
|
+
color0="#14b143",
|
421
|
+
border_color="#ef232a",
|
422
|
+
border_color0="#14b143",
|
423
|
+
)
|
424
|
+
line.add_yaxis(
|
425
|
+
data_item["series_name"], data_list, itemstyle_opts=itemstyle_opts
|
426
|
+
)
|
427
|
+
elif data_item["draw_type"] == "Bar":
|
428
|
+
if "check_col" in data_item:
|
429
|
+
random_uuid = str(uuid.uuid4()).replace("-", "")
|
430
|
+
line.add_yaxis(
|
431
|
+
series_name=data_item["series_name"],
|
432
|
+
y_axis=draw_df[col].tolist(),
|
433
|
+
label_opts=opts.LabelOpts(is_show=False),
|
434
|
+
itemstyle_opts=opts.ItemStyleOpts(
|
435
|
+
color=JsCode(
|
436
|
+
f"""
|
437
|
+
function(params) {{\n
|
438
|
+
var colorList;
|
439
|
+
if (check_{random_uuid}[params.dataIndex] > 0) {{
|
440
|
+
colorList = '#ef232a';
|
441
|
+
}} else {{
|
442
|
+
colorList = '#14b143';
|
443
|
+
}}
|
444
|
+
return colorList;
|
445
|
+
}}
|
446
|
+
"""
|
447
|
+
)
|
448
|
+
),
|
449
|
+
)
|
450
|
+
grid_chart.add_js_funcs(
|
451
|
+
f"var check_{random_uuid} = {draw_df[data_item['check_col']].tolist()}"
|
452
|
+
)
|
453
|
+
else:
|
454
|
+
line.add_yaxis(
|
455
|
+
series_name=data_item["series_name"],
|
456
|
+
y_axis=draw_df[col].tolist(),
|
457
|
+
label_opts=opts.LabelOpts(is_show=False),
|
458
|
+
itemstyle_opts=itemstyle_opts,
|
459
|
+
)
|
460
|
+
elif data_item["draw_type"] == "DownLine":
|
461
|
+
line.add_yaxis(
|
462
|
+
series_name=data_item["series_name"],
|
463
|
+
y_axis=draw_df[col].tolist(),
|
464
|
+
itemstyle_opts=opts.ItemStyleOpts(
|
465
|
+
color="#fdd07d", border_color="#fdd07d", border_width=0
|
466
|
+
),
|
467
|
+
is_symbol_show=data_item["is_symbol_show"],
|
468
|
+
areastyle_opts=opts.AreaStyleOpts(opacity=0.5),
|
469
|
+
linestyle_opts=linestyle_opts,
|
470
|
+
label_opts=opts.LabelOpts(is_show=False),
|
471
|
+
)
|
472
|
+
else:
|
473
|
+
line.add_yaxis(
|
474
|
+
series_name=data_item["series_name"],
|
475
|
+
y_axis=draw_df[col].tolist(),
|
476
|
+
is_smooth=data_item["is_smooth"],
|
477
|
+
label_opts=opts.LabelOpts(is_show=False),
|
478
|
+
is_symbol_show=data_item["is_symbol_show"],
|
479
|
+
linestyle_opts=linestyle_opts,
|
480
|
+
itemstyle_opts=itemstyle_opts,
|
481
|
+
)
|
482
|
+
|
483
|
+
if i == 0: # 如果是第0个图表,要设置一系列的参数实现后面的多图联动
|
484
|
+
_set_first_chart(line, title, cur_top, chart_count, i, pic_width, height_type)
|
485
|
+
else:
|
486
|
+
_set_other_chart(line, cur_top, chart_count, i, pic_width, height_type)
|
487
|
+
virtual_map_js = produce_split_color_js(
|
488
|
+
draw_df, data_item, ii
|
489
|
+
) # 生成曲线分段显示不同颜色的配置
|
490
|
+
mark_point_js = _produce_mark_point(
|
491
|
+
main_df, date_col, draw_df, data_item, ii, time_data
|
492
|
+
)
|
493
|
+
return line, js_code, virtual_map_js, mark_point_js
|
494
|
+
|
495
|
+
|
496
|
+
def _produce_x_date(draw_df: pd.DataFrame, date_col: str, date_formate: str) -> list:
|
497
|
+
"""
|
498
|
+
计算时间轴数据
|
499
|
+
:param draw_df:数据集
|
500
|
+
:param date_col: 日期列名
|
501
|
+
:param date_formate: 日期显示格式
|
502
|
+
"""
|
503
|
+
# 计算时间轴,如果传入了data_col,则取该列作为时间轴,否则取index作为时间轴
|
504
|
+
if date_col:
|
505
|
+
if hasattr(draw_df[date_col], "dt"):
|
506
|
+
time_data = draw_df[date_col].dt.strftime(date_formate).tolist()
|
507
|
+
else:
|
508
|
+
time_data = draw_df[date_col].tolist()
|
509
|
+
else:
|
510
|
+
if hasattr(draw_df[date_col], "dt"):
|
511
|
+
time_data = draw_df.index.dt.strftime(date_formate).tolist()
|
512
|
+
else:
|
513
|
+
time_data = draw_df.index.tolist()
|
514
|
+
return time_data
|
515
|
+
|
516
|
+
|
517
|
+
def _produce_change_log_js(data_item, cur_top, i, height_type) -> str:
|
518
|
+
"""
|
519
|
+
给当前图表加上切换 对数显示 的相关脚本
|
520
|
+
:param data_item: 当前图表配置信息
|
521
|
+
:param cur_top: 当前空白区域最上面的纵坐标
|
522
|
+
:param i: 当前是第几个图表
|
523
|
+
"""
|
524
|
+
js_code = ""
|
525
|
+
if height_type == "%":
|
526
|
+
top = f"{cur_top}%"
|
527
|
+
else:
|
528
|
+
top = f"{cur_top}px"
|
529
|
+
if "can_change_log" in data_item and data_item["can_change_log"]:
|
530
|
+
# 添加 JavaScript 代码实现切换坐标系功能
|
531
|
+
js_code = f"""
|
532
|
+
<div style='position: absolute;top: {top};right:{left_data_div_width + 120}px'>
|
533
|
+
<select id="log_select_{i}" style="font-size: 15px;width: 70px;height: 30px;">
|
534
|
+
<option value="value">线性</option>
|
535
|
+
<option value="log">对数</option>
|
536
|
+
</select>
|
537
|
+
</div>
|
538
|
+
<script>
|
539
|
+
document.getElementById('log_select_{i}').onchange = function(){{
|
540
|
+
let cc=document.getElementsByClassName("chart-container");
|
541
|
+
let cc_name=cc[0].id;
|
542
|
+
let option=window["option_"+cc_name];
|
543
|
+
option.yAxis[{i}].type=this.value;
|
544
|
+
let ccv=window["chart_"+cc_name];
|
545
|
+
ccv.setOption(option);
|
546
|
+
}}
|
547
|
+
</script>
|
548
|
+
|
549
|
+
"""
|
550
|
+
return js_code
|
551
|
+
|
552
|
+
|
553
|
+
def _produce_date_opt_js(
|
554
|
+
pic_width: int,
|
555
|
+
display_js: str,
|
556
|
+
height_type: str,
|
557
|
+
title,
|
558
|
+
zoom_space,
|
559
|
+
zoom_end,
|
560
|
+
right_data_view,
|
561
|
+
):
|
562
|
+
"""
|
563
|
+
生成日期跳转操作相关的脚本
|
564
|
+
"""
|
565
|
+
display_ext = "none" # 扩展数据区是否显示
|
566
|
+
ext_data_div_width = 0 # 扩展数据区显示宽度
|
567
|
+
if display_js is not None:
|
568
|
+
display_ext = ""
|
569
|
+
ext_data_div_width = left_data_div_width
|
570
|
+
view_data_js = f"""
|
571
|
+
let data_div = document.getElementById('data_div');
|
572
|
+
let conn= "<table style='font-size: 12px;word-break: break-word;width:100%'>"
|
573
|
+
+"<tr><td style='background-color: #f7f7f7;;border-right: 1px dashed #000;padding: 5px 0;font-weight: bold;'>当前时间</td>"
|
574
|
+
+"<td style='background-color: #f7f7f7;'>"+chart_option.xAxis[0].data[ei]+"</td>"
|
575
|
+
for (let i=0;i<chart_option.series.length;i++){{
|
576
|
+
let b_color= "color" in line_other_param[i]?line_other_param[i].color:"#101010";
|
577
|
+
// b_color = darkenColor(b_color); //本来是准备给颜色减淡一些做背景的,有问题先不处理了
|
578
|
+
conn = conn + "<tr><td style='color:"+b_color+";background-color:#f7f7f7;border-right: 1px dashed #000;border-top: 1px dashed #000;padding: 1px 0;font-weight: bold;'>"
|
579
|
+
+chart_option.series[i].name+"</td><td style='border-top: 1px dashed #000;'>";
|
580
|
+
let datacon="";
|
581
|
+
let d=chart_option.series[i].data[ei];
|
582
|
+
let dec_length=line_other_param[i].dec_length;
|
583
|
+
if (chart_option.series[i].type=="line"){{
|
584
|
+
d=d[1];
|
585
|
+
d=fixDecLen(d,dec_length);
|
586
|
+
datacon+="<span style='color:"+(d>0?"red":"green")+"'>"+d+"</span>";
|
587
|
+
}}else if (chart_option.series[i].type=="candlestick"){{
|
588
|
+
let zdf=d[1]/d[0]-1;
|
589
|
+
zdf=(zdf*100).toFixed(2);
|
590
|
+
datacon+="开("+fixDecLen(d[0],dec_length)+"),收("+fixDecLen(d[1],dec_length)+"),<br/>低("
|
591
|
+
+d[2]+"),高("+fixDecLen(d[3],dec_length)+"),<br/>涨:<span style='color:"+(zdf>0?"red":"green")+"';>"
|
592
|
+
+fixDecLen(zdf,dec_length)+"%</span>";
|
593
|
+
}}else{{
|
594
|
+
datacon+=d;
|
595
|
+
}}
|
596
|
+
conn = conn + datacon;
|
597
|
+
conn = conn +"</td></tr>"
|
598
|
+
}}
|
599
|
+
conn = conn + "</table>";
|
600
|
+
data_div.innerHTML = conn;
|
601
|
+
"""
|
602
|
+
js_code = f"""
|
603
|
+
<style>
|
604
|
+
body {{
|
605
|
+
margin: 0;
|
606
|
+
}}
|
607
|
+
.button {{
|
608
|
+
border: 0px solid #ccc; /* 设置按钮的边框 */
|
609
|
+
background-color: transparent; /* 设置按钮的背景颜色为透明 */
|
610
|
+
padding: 5px 10px 5px 10px; /* 设置按钮的内边距 */
|
611
|
+
cursor: pointer; /* 设置按钮的鼠标样式为手型 */
|
612
|
+
font-size: 13px; /* 设置按钮的字体大小 */
|
613
|
+
}}
|
614
|
+
.button:hover {{
|
615
|
+
background-color: darkorange; /* 设置按钮在鼠标悬停时的背景颜色 */
|
616
|
+
color: white;
|
617
|
+
}}
|
618
|
+
</style>
|
619
|
+
<div style="position: fixed;left: 40px;top: 40px;bottom: 0;width: 3px;background-color: #e0e3eb;"></div>
|
620
|
+
<div style="position: fixed;right: {left_data_div_width}px;top: 40px;bottom: 0;width: 3px;background-color: #e0e3eb;"></div>
|
621
|
+
<div style="position: fixed;left: 0;right:0;top: 40px;height: 3px;background-color: #e0e3eb;"></div>
|
622
|
+
<div style="position: fixed;left: 0px;height: 40px;top: 0;right: 0;background-color:#f9f9f9;display: flex;">
|
623
|
+
<div style="padding: 10px 10px 5px 5px;font-size: 16px;font-weight: bold;">{title}</div>
|
624
|
+
<div style="margin: 8px 0px;width: 1px;height:25px;background-color: #b4b8c3;"></div>
|
625
|
+
<div style='display: flex;padding: 5px;font-size: 13px;'>
|
626
|
+
<label for="date-input" style="padding-top: 7px;">日期:</label>
|
627
|
+
<input id="date-input" name="date" style="width:80px">
|
628
|
+
<button class="button" type="submit" id="jump_bt">跳转</button>
|
629
|
+
<button class="button" style="display: flex;padding-top: 7px;" type="submit" id="to_front">
|
630
|
+
➡️ 
|
631
|
+
<span>前进</span>
|
632
|
+
</button>
|
633
|
+
<button class="button" style="display: flex;padding-top: 7px;" type="submit" id="to_back">
|
634
|
+
⬅️ 
|
635
|
+
<span>回退</span>
|
636
|
+
|
637
|
+
</button>
|
638
|
+
<button class="button" type="submit" id="auto_play">自动前进</button>
|
639
|
+
</div>
|
640
|
+
<div style="margin: 8px 0px;width: 1px;height:25px;background-color: #b4b8c3;"></div>
|
641
|
+
<div style='display: flex;padding: 5px;font-size: 13px;'>
|
642
|
+
<label for="date-input" style="padding-top: 7px;">时间窗口宽度:</label>
|
643
|
+
<input id="date_win_width" name="date_win_width" style="width:60px">
|
644
|
+
<button class="button" type="submit" id="date_win_width_set">设置</button>
|
645
|
+
</div>
|
646
|
+
<div style="margin: 8px 0px;width: 1px;height:25px;background-color: #b4b8c3;"></div>
|
647
|
+
<div id="inf_div" style='border: 0px solid black;font-size:14px;color: red;padding: 10px;'>
|
648
|
+
信息显示区
|
649
|
+
</div>
|
650
|
+
</div>
|
651
|
+
|
652
|
+
<div id="all_data_div" style='position: absolute;top: 45px;right:0px;width:{left_data_div_width}px;height:auto;border: 0px solid black;font-size:14px;'>
|
653
|
+
<div id="data_div" style='display:{"" if right_data_view else "none"};'>
|
654
|
+
数据显示区
|
655
|
+
</div>
|
656
|
+
<div style="left: 0;right:0;height: 3px;background-color: #e0e3eb;display:{"" if right_data_view else "none"};"></div>
|
657
|
+
<div id="ext_data_div" style='display:{display_ext};width:{ext_data_div_width}px;border: 0px solid black;font-size:14px;'>
|
658
|
+
扩展信息显示
|
659
|
+
</div>
|
660
|
+
<div style="display:{display_ext};left: 0;right:0;height: 3px;background-color: #e0e3eb;"></div>
|
661
|
+
</div>
|
662
|
+
<script>
|
663
|
+
//获取chart实例
|
664
|
+
function play_ibe_js_default(ibe,next){{
|
665
|
+
//如果next为true表示向后移动,false为向前移动,null为不移动
|
666
|
+
if(next===true){{
|
667
|
+
ibe.begin += 1
|
668
|
+
ibe.end += 1;
|
669
|
+
}}else if(next===false){{
|
670
|
+
ibe.begin -= 1
|
671
|
+
ibe.end -= 1;
|
672
|
+
}}
|
673
|
+
ibe.begin = ibe.begin<0?0:ibe.begin;
|
674
|
+
ibe.end = ibe.end<10?10:ibe.end;
|
675
|
+
return ibe;
|
676
|
+
}}
|
677
|
+
|
678
|
+
function play_ibe(ibe,next){{
|
679
|
+
if (typeof play_ibe_js=="function"){{
|
680
|
+
return play_ibe_js(ibe,next);
|
681
|
+
}}else{{
|
682
|
+
return play_ibe_js_default(ibe,next);
|
683
|
+
}}
|
684
|
+
}}
|
685
|
+
|
686
|
+
function cleanInf(){{
|
687
|
+
document.getElementById("inf_div").innerHTML=""
|
688
|
+
}}
|
689
|
+
function setInf(info,view_sed){{
|
690
|
+
document.getElementById("inf_div").innerHTML=info
|
691
|
+
if (view_sed!=null && view_sed>0){{
|
692
|
+
setTimeout(cleanInf, 5000); // 1000毫秒后清除提示
|
693
|
+
}}
|
694
|
+
}}
|
695
|
+
function darkenColor(colorStr, percent) {{
|
696
|
+
const f = parseInt(colorStr.slice(1), 16);
|
697
|
+
const t = (f * percent) >> 16;
|
698
|
+
return "#" + (0x10000 + t).toString(16).slice(1);
|
699
|
+
}}
|
700
|
+
|
701
|
+
// 监听 datazoom 事件
|
702
|
+
chart_ins.on('datazoom', function (params) {{
|
703
|
+
// 获取当前 dataZoom 的 startValue 和 endValue
|
704
|
+
date_data=chart_option.xAxis[0].data;
|
705
|
+
let bi=0,ei=0;
|
706
|
+
if (params.start>=0){{
|
707
|
+
dataZoom_startValue_i = Math.ceil(date_data.length*params.start/100);
|
708
|
+
dataZoom_startValue_i=dataZoom_startValue_i<0?0:dataZoom_startValue_i;
|
709
|
+
dataZoom_endValue_i = Math.ceil(date_data.length*params.end/100);
|
710
|
+
dataZoom_endValue_i=dataZoom_endValue_i>date_data.length-1?date_data.length-1:dataZoom_endValue_i;
|
711
|
+
bi = dataZoom_startValue_i;
|
712
|
+
ei = dataZoom_endValue_i;
|
713
|
+
dataZoom_startValue="";
|
714
|
+
dataZoom_endValue="";
|
715
|
+
// 输出 startValue 和 endValue
|
716
|
+
//console.log("startValue:", date_data[dataZoom_startValue_i]);
|
717
|
+
//console.log("endValue:", date_data[dataZoom_endValue_i]);
|
718
|
+
}}else{{
|
719
|
+
dataZoom_startValue=params.startValue;
|
720
|
+
dataZoom_endValue=params.endValue;
|
721
|
+
dataZoom_startValue_i=-1;
|
722
|
+
dataZoom_endValue_i=-1;
|
723
|
+
for (let i=0;i<chart_option.xAxis[0].data.length;i++){{
|
724
|
+
if (chart_option.xAxis[0].data[i]==dataZoom_startValue){{
|
725
|
+
bi=i;
|
726
|
+
}}
|
727
|
+
if (chart_option.xAxis[0].data[i]==dataZoom_endValue){{
|
728
|
+
ei=i;
|
729
|
+
}}
|
730
|
+
}}
|
731
|
+
dataZoom_startValue_i=bi;
|
732
|
+
dataZoom_endValue_i=ei;
|
733
|
+
// 输出 startValue 和 endValue
|
734
|
+
//console.log("startValue:", dataZoom_startValue);
|
735
|
+
//console.log("endValue:", dataZoom_endValue);
|
736
|
+
}}
|
737
|
+
dataZoom_endValue_i=ei;
|
738
|
+
//归1化显示设置window_cumprod_base_one的数据,窗口内的数据根据涨跌幅算净值
|
739
|
+
let opt_changed=false;
|
740
|
+
for (let i=0;i<chart_option.series.length;i++){{
|
741
|
+
if (line_other_param[i].window_cumprod_base_one && chart_option.series[i].type=="line"){{
|
742
|
+
opt_changed=true;
|
743
|
+
chart_option.series[i].data[bi][1] = 1;
|
744
|
+
for (let j=bi+1;j<=ei;j++){{
|
745
|
+
chart_option.series[i].data[j][1] = chart_option.series[i].data[j-1][1] * (1+chart_option_back.series[i].data[j][1]);
|
746
|
+
}}
|
747
|
+
}}
|
748
|
+
}}
|
749
|
+
if (opt_changed==true){{
|
750
|
+
delete chart_option.dataZoom[0].start;
|
751
|
+
delete chart_option.dataZoom[0].end;
|
752
|
+
chart_option.dataZoom[0].startValue=bi;
|
753
|
+
chart_option.dataZoom[0].endValue=ei;
|
754
|
+
chart_ins.setOption(chart_option);
|
755
|
+
}}
|
756
|
+
//显示当前结束点的数据
|
757
|
+
{view_data_js if right_data_view else ""}
|
758
|
+
let d_width=dataZoom_endValue_i-dataZoom_startValue_i+1;
|
759
|
+
if (typeof set_ext_data_div === 'function'){{
|
760
|
+
set_ext_data_div(document.getElementById("ext_data_div"),ei,d_width);
|
761
|
+
}}
|
762
|
+
document.getElementById('date_win_width').value=d_width;
|
763
|
+
}});
|
764
|
+
//时间窗口宽度设置
|
765
|
+
let date_win_width_set = document.getElementById('date_win_width_set');
|
766
|
+
date_win_width_set.addEventListener('click', function() {{
|
767
|
+
let ibe={{begin:dataZoom_startValue_i,end:dataZoom_endValue_i}};
|
768
|
+
let d_width = document.getElementById('date_win_width').value-1;
|
769
|
+
ibe.begin = (ibe.end-d_width)>0?(ibe.end-d_width):0;
|
770
|
+
chart_ins.dispatchAction({{
|
771
|
+
type: 'dataZoom',
|
772
|
+
startValue: chart_option.xAxis[0].data[ibe.begin],
|
773
|
+
endValue: chart_option.xAxis[0].data[ibe.end],
|
774
|
+
}});
|
775
|
+
}});
|
776
|
+
//自动回放
|
777
|
+
let auto_play = document.getElementById('auto_play');
|
778
|
+
let is_run_play=false;
|
779
|
+
auto_play.addEventListener('click', function() {{
|
780
|
+
if (!is_run_play){{
|
781
|
+
auto_play.innerText="停止";
|
782
|
+
is_run_play=true;
|
783
|
+
do_next();
|
784
|
+
}}else{{
|
785
|
+
auto_play.innerText="自动回放";
|
786
|
+
is_run_play=false;
|
787
|
+
}}
|
788
|
+
function do_next(){{
|
789
|
+
if (is_run_play){{
|
790
|
+
if (chart_option.xAxis[0].data.length-1>dataZoom_endValue_i+1){{
|
791
|
+
let ibe={{begin:dataZoom_startValue_i,end:dataZoom_endValue_i}};
|
792
|
+
ibe=play_ibe(ibe,true);
|
793
|
+
chart_ins.dispatchAction({{
|
794
|
+
type: 'dataZoom',
|
795
|
+
startValue: chart_option.xAxis[0].data[ibe.begin],
|
796
|
+
endValue: chart_option.xAxis[0].data[ibe.end],
|
797
|
+
}});
|
798
|
+
}}else{{
|
799
|
+
setInf("已经前进到最后一天",2000);
|
800
|
+
auto_play.innerText="自动回放";
|
801
|
+
return;
|
802
|
+
}}
|
803
|
+
let space=2000;
|
804
|
+
if (typeof auto_play_space === 'function'){{
|
805
|
+
space=auto_play_space(dataZoom_endValue_i);
|
806
|
+
}}
|
807
|
+
setTimeout(do_next, space); // 1000 毫秒后再次执行自己
|
808
|
+
}}
|
809
|
+
}}
|
810
|
+
}});
|
811
|
+
//向前跳转
|
812
|
+
let to_front = document.getElementById('to_front');
|
813
|
+
to_front.addEventListener('click', function() {{
|
814
|
+
if (chart_option.xAxis[0].data.length-1>dataZoom_endValue_i+1){{
|
815
|
+
let ibe={{begin:dataZoom_startValue_i,end:dataZoom_endValue_i}};
|
816
|
+
ibe=play_ibe(ibe,true);
|
817
|
+
chart_ins.dispatchAction({{
|
818
|
+
type: 'dataZoom',
|
819
|
+
startValue: chart_option.xAxis[0].data[ibe.begin],
|
820
|
+
endValue: chart_option.xAxis[0].data[ibe.end],
|
821
|
+
}});
|
822
|
+
}}else{{
|
823
|
+
setInf("已经前进到最后一天",2000);
|
824
|
+
}}
|
825
|
+
|
826
|
+
}});
|
827
|
+
// 向后跳转
|
828
|
+
let to_back = document.getElementById('to_back');
|
829
|
+
to_back.addEventListener('click', function() {{
|
830
|
+
if (dataZoom_endValue_i-1>=10){{
|
831
|
+
let ibe={{begin:dataZoom_startValue_i,end:dataZoom_endValue_i}};
|
832
|
+
ibe=play_ibe(ibe,false);
|
833
|
+
chart_ins.dispatchAction({{
|
834
|
+
type: 'dataZoom',
|
835
|
+
startValue: chart_option.xAxis[0].data[ibe.begin],
|
836
|
+
endValue: chart_option.xAxis[0].data[ibe.end],
|
837
|
+
}});
|
838
|
+
}}else{{
|
839
|
+
setInf("已经回退到第一天",2000);
|
840
|
+
}}
|
841
|
+
}});
|
842
|
+
// 日期输入框跳转
|
843
|
+
let jump_bt = document.getElementById('jump_bt');
|
844
|
+
jump_bt.addEventListener('click', function() {{
|
845
|
+
let dateInput = document.getElementById('date-input');
|
846
|
+
endDate=dateInput.value;
|
847
|
+
if (endDate.indexOf("-")<0 && endDate.length>=6){{
|
848
|
+
endDate=endDate.replace(/({{4}})({{2}})({{2}})/, "$1-$2-$3");
|
849
|
+
}}
|
850
|
+
to_date(endDate);
|
851
|
+
}});
|
852
|
+
|
853
|
+
function to_date(endDate){{
|
854
|
+
//设置chart的dataZoom实现X轴变化
|
855
|
+
//从后向前第一个小于等于该日期的日期/或者X轴中的一个日期,修正endDate,因为有可能用户输入的日期在X轴中不存在,或者大于X轴的最大值,或者小于X轴的最小值
|
856
|
+
let endDate_i=-1;
|
857
|
+
for (let i=0;i<chart_option.xAxis[0].data.length;i++){{
|
858
|
+
if (chart_option.xAxis[0].data[i]>=endDate){{
|
859
|
+
endDate=chart_option.xAxis[0].data[i]
|
860
|
+
endDate_i=i;
|
861
|
+
break;
|
862
|
+
}}
|
863
|
+
}}
|
864
|
+
let space = dataZoom_endValue_i-dataZoom_startValue_i;
|
865
|
+
let ibe={{end:endDate_i}};
|
866
|
+
ibe.begin=ibe.end-space>=0?ibe.end-space:0;
|
867
|
+
ibe=play_ibe(ibe,null);
|
868
|
+
chart_ins.dispatchAction({{
|
869
|
+
type: 'dataZoom',
|
870
|
+
startValue: chart_option.xAxis[0].data[ibe.begin],
|
871
|
+
endValue: chart_option.xAxis[0].data[ibe.end],
|
872
|
+
}});
|
873
|
+
}}
|
874
|
+
//默认跳到最后,并显示zoom_space个点的数据
|
875
|
+
let d_l=chart_option.xAxis[0].data.length-1;
|
876
|
+
let zoom_end={-1 if zoom_end is None else zoom_end};
|
877
|
+
d_l = zoom_end>0?zoom_end:d_l;
|
878
|
+
d_endValue=chart_option.xAxis[0].data[d_l];
|
879
|
+
let d_startValue=chart_option.xAxis[0].data[d_l>{zoom_space}?d_l-{zoom_space}+1:0];
|
880
|
+
chart_ins.dispatchAction({{
|
881
|
+
type: 'dataZoom',
|
882
|
+
startValue: d_startValue,
|
883
|
+
endValue: d_endValue,
|
884
|
+
}});
|
885
|
+
// 监听点击事件,实现点击曲线隐藏该曲线
|
886
|
+
chart_ins.on('click', (params) => {{
|
887
|
+
if (params.seriesType === 'line') {{
|
888
|
+
let seriesName = params.seriesName;
|
889
|
+
chart_ins.dispatchAction({{
|
890
|
+
type: 'legendUnSelect',
|
891
|
+
name: seriesName,
|
892
|
+
}});
|
893
|
+
}}
|
894
|
+
}});
|
895
|
+
"""
|
896
|
+
# 如果高度是用%设置的,要监听浏览器窗口大小变化事件实现图表自动变大小
|
897
|
+
if height_type == "%":
|
898
|
+
resize_js = """
|
899
|
+
function resize_chart(){{
|
900
|
+
// 获取当前窗口的宽度和高度
|
901
|
+
let width = window.innerWidth;
|
902
|
+
let height = window.innerHeight;
|
903
|
+
|
904
|
+
// 将 id 为 "a" 的 div 大小设置为与页面一样
|
905
|
+
//cc[0].style.width = width + 'px';
|
906
|
+
cc[0].style.height = height + 'px';
|
907
|
+
chart_ins.resize();
|
908
|
+
}}
|
909
|
+
// 监听窗口大小变化事件
|
910
|
+
window.addEventListener('resize', function () {{
|
911
|
+
resize_chart();
|
912
|
+
}});
|
913
|
+
resize_chart();
|
914
|
+
"""
|
915
|
+
js_code += resize_js
|
916
|
+
js_code += "</script>"
|
917
|
+
return js_code
|
918
|
+
|
919
|
+
|
920
|
+
def _set_other_chart(chart, cur_top, chart_count, i, pic_width, height_type):
|
921
|
+
"""
|
922
|
+
设置其它图表的各种参数
|
923
|
+
:param chart: 图表
|
924
|
+
:param cur_top: 当前空白区域最上面的纵坐标
|
925
|
+
:param chart_count: 总计图表数,主要用在生成第一个图表时配置项中的多表联动部分需要知道一共有多少个图表
|
926
|
+
:param i: 当前是第几个图表
|
927
|
+
"""
|
928
|
+
if height_type == "%":
|
929
|
+
legend_top = f"{cur_top}%"
|
930
|
+
else:
|
931
|
+
legend_top = cur_top
|
932
|
+
|
933
|
+
# v_m = None
|
934
|
+
# if data_item["draw_type"]=="Line":
|
935
|
+
# v_m = opts.VisualMapOpts(
|
936
|
+
# is_piecewise=True, # 设置为分段型
|
937
|
+
# is_show=False,
|
938
|
+
# dimension=0,
|
939
|
+
# # series_index=i,
|
940
|
+
# pieces=[
|
941
|
+
# {"lte": 10*i, "color": "#FF0000"}, # 定义分段区间及对应颜色
|
942
|
+
# {"gt": 10*i, "lte": 40*i, "color": "#00FF00"},
|
943
|
+
# {"gt": 40*i, "color": "#0000FF"},
|
944
|
+
# ],
|
945
|
+
# )
|
946
|
+
|
947
|
+
chart.set_global_opts(
|
948
|
+
tooltip_opts=opts.TooltipOpts(
|
949
|
+
trigger="axis", axis_pointer_type="cross"
|
950
|
+
), # 设置鼠标悬停提示
|
951
|
+
legend_opts=opts.LegendOpts(
|
952
|
+
orient="vertical",
|
953
|
+
pos_left=f"{45}",
|
954
|
+
pos_top=legend_top,
|
955
|
+
# is_show=True, pos_top=25, pos_left="center"
|
956
|
+
),
|
957
|
+
# visualmap_opts=v_m,
|
958
|
+
yaxis_opts=opts.AxisOpts(
|
959
|
+
is_scale=True,
|
960
|
+
position="right",
|
961
|
+
splitarea_opts=opts.SplitAreaOpts(
|
962
|
+
is_show=True, areastyle_opts=opts.AreaStyleOpts(opacity=1)
|
963
|
+
),
|
964
|
+
),
|
965
|
+
xaxis_opts=opts.AxisOpts(
|
966
|
+
axislabel_opts=opts.LabelOpts(
|
967
|
+
is_show=(i == chart_count - 1)
|
968
|
+
) # 设置 interval 为 None,表示不显示轴下标
|
969
|
+
),
|
970
|
+
)
|
971
|
+
|
972
|
+
|
973
|
+
def _set_first_chart(
|
974
|
+
chart, title, cur_top, chart_count, i, pic_width, height_type
|
975
|
+
) -> None:
|
976
|
+
"""
|
977
|
+
给第一个图表设置各种参数,实现多个图表的联动显示
|
978
|
+
:param chart: 图表
|
979
|
+
:param title: 标题
|
980
|
+
:param cur_top: 当前空白区域最上面的纵坐标
|
981
|
+
:param chart_count: 总计图表数,主要用在生成第一个图表时配置项中的多表联动部分需要知道一共有多少个图表
|
982
|
+
:param i: 当前是第几个图表
|
983
|
+
"""
|
984
|
+
# res += '<span style="display:inline-block;margin-right:4px;border-radius:10px;width:10px;height:10px;background-color:' + params[i].color + ';"></span>';
|
985
|
+
# 格式化提示框内容的函数
|
986
|
+
legend_top = top_height
|
987
|
+
tooltip_opts = opts.TooltipOpts( # 鼠标在图表中移动时显示所有图表的当前值
|
988
|
+
trigger="axis",
|
989
|
+
axis_pointer_type="cross",
|
990
|
+
background_color="rgba(245, 245, 245, 0.8)",
|
991
|
+
border_width=1,
|
992
|
+
border_color="#ccc",
|
993
|
+
textstyle_opts=opts.TextStyleOpts(color="#000", font_size=13),
|
994
|
+
# position=[20, 20],
|
995
|
+
# formatter=JsCode(js_code_str),
|
996
|
+
)
|
997
|
+
|
998
|
+
chart.set_global_opts(
|
999
|
+
title_opts=opts.TitleOpts(is_show=False),
|
1000
|
+
# 在整个图表的最上面的中间显示title
|
1001
|
+
legend_opts=opts.LegendOpts( # 在左侧显示当前图表的数据图例
|
1002
|
+
orient="vertical",
|
1003
|
+
pos_left=f"{45}",
|
1004
|
+
# pos_right="10%",
|
1005
|
+
pos_top=legend_top,
|
1006
|
+
# align="left",
|
1007
|
+
# is_show=True, pos_top=25, pos_left="center"
|
1008
|
+
),
|
1009
|
+
tooltip_opts=tooltip_opts,
|
1010
|
+
toolbox_opts=opts.ToolboxOpts(
|
1011
|
+
orient="vertical",
|
1012
|
+
pos_left="left",
|
1013
|
+
pos_top="43",
|
1014
|
+
), # 显示工具箱
|
1015
|
+
datazoom_opts=[ # 所有图表共用同一个data_zoom
|
1016
|
+
opts.DataZoomOpts(
|
1017
|
+
is_show=True,
|
1018
|
+
xaxis_index=list(range(0, chart_count)),
|
1019
|
+
type_="slider",
|
1020
|
+
pos_bottom="15",
|
1021
|
+
# pos_top="95%",
|
1022
|
+
range_start=80,
|
1023
|
+
range_end=100,
|
1024
|
+
),
|
1025
|
+
],
|
1026
|
+
yaxis_opts=opts.AxisOpts(
|
1027
|
+
is_scale=True,
|
1028
|
+
position="right",
|
1029
|
+
splitarea_opts=opts.SplitAreaOpts(
|
1030
|
+
is_show=True, areastyle_opts=opts.AreaStyleOpts(opacity=1)
|
1031
|
+
),
|
1032
|
+
),
|
1033
|
+
# visualmap_opts=opts.VisualMapOpts(
|
1034
|
+
# is_show=False,
|
1035
|
+
# dimension=2,
|
1036
|
+
# series_index=5,
|
1037
|
+
# is_piecewise=True,
|
1038
|
+
# pieces=[
|
1039
|
+
# {"value": 1, "color": "#00da3c"},
|
1040
|
+
# {"value": -1, "color": "#ec0000"},
|
1041
|
+
# ],
|
1042
|
+
# ),
|
1043
|
+
axispointer_opts=opts.AxisPointerOpts(
|
1044
|
+
is_show=True,
|
1045
|
+
link=[{"xAxisIndex": "all"}],
|
1046
|
+
label=opts.LabelOpts(background_color="#777"),
|
1047
|
+
),
|
1048
|
+
brush_opts=opts.BrushOpts(
|
1049
|
+
x_axis_index="all",
|
1050
|
+
brush_link="all",
|
1051
|
+
out_of_brush={"colorAlpha": 0.1},
|
1052
|
+
brush_type="lineX",
|
1053
|
+
),
|
1054
|
+
xaxis_opts=opts.AxisOpts(
|
1055
|
+
axislabel_opts=opts.LabelOpts(
|
1056
|
+
is_show=(i == chart_count - 1)
|
1057
|
+
) # 设置 interval 为 None,表示不显示轴下标
|
1058
|
+
),
|
1059
|
+
)
|
1060
|
+
|
1061
|
+
|
1062
|
+
def produce_split_color_js(df, data_item, ii):
|
1063
|
+
"""
|
1064
|
+
生成当前图表的分段显示不同颜色的脚本,这个本来是通过pyecharts实现的,但是发现有多个曲线要不段颜色显示时pyecharts有bug,改成直接操作echarts的operation参数实现
|
1065
|
+
:params df:图表数据所在的df
|
1066
|
+
:params data_item: 图表配置项
|
1067
|
+
:params ii: 算上子图表在内当前是第几个图表
|
1068
|
+
"""
|
1069
|
+
# "split_color_col": 不必填,默认为None。轮动资金曲线或净值曲线需要根据不同的子策略分段显示不同的颜色时使用,本属性表示用df中的哪个列判断子策略。上面是业务角度的描述,通用的描述就是曲线需要根据df中的某列显示不同的颜色场景。
|
1070
|
+
# "split_color": 不必填,默认为None,但是如果split_color_col有值,则本参数一定要配置。当split_color_col有值时,可以配置本属性,用来描述每个子策略对应的颜色
|
1071
|
+
js = ""
|
1072
|
+
if "split_color_col" in data_item:
|
1073
|
+
split_color_col = data_item["split_color_col"]
|
1074
|
+
# 使用 groupby() 和 cumcount() 函数来计算连续区间
|
1075
|
+
df["group"] = (
|
1076
|
+
df[split_color_col] != df[split_color_col].shift()
|
1077
|
+
).cumsum() # 生成一个分组标识符
|
1078
|
+
result = (
|
1079
|
+
df.groupby([split_color_col, "group"])
|
1080
|
+
.apply(lambda x: (x[split_color_col].iloc[0], x.index[0], x.index[-1]))
|
1081
|
+
.tolist()
|
1082
|
+
)
|
1083
|
+
# 输出结果
|
1084
|
+
pieces = ""
|
1085
|
+
i = 0
|
1086
|
+
for item in result:
|
1087
|
+
# print(f"连续区间:{item[0]},起始行:{item[1]},结束行:{item[2]}")
|
1088
|
+
if item[0].strip() in data_item["split_color"]:
|
1089
|
+
pieces += f"""
|
1090
|
+
{{
|
1091
|
+
"gte": {item[1]},
|
1092
|
+
"lte": {item[2] + 1},
|
1093
|
+
"color": "{data_item["split_color"][item[0].strip()]}"
|
1094
|
+
}},
|
1095
|
+
"""
|
1096
|
+
i += 1
|
1097
|
+
js = f"""
|
1098
|
+
{{
|
1099
|
+
"show": false,
|
1100
|
+
"type": "piecewise",
|
1101
|
+
"dimension": 0,
|
1102
|
+
"seriesIndex": {ii},
|
1103
|
+
"pieces": [
|
1104
|
+
{pieces}
|
1105
|
+
]
|
1106
|
+
}},
|
1107
|
+
"""
|
1108
|
+
return js
|
1109
|
+
|
1110
|
+
|
1111
|
+
def _produce_mark_point(
|
1112
|
+
df: pd.DataFrame, date_col: str, draw_df: pd.DataFrame, data_item, ii, time_data
|
1113
|
+
):
|
1114
|
+
"""
|
1115
|
+
:params buy_sale_single: 不必填,默认为None,买入卖出信号列,如果该属性配置了,则图上会根据该列的值如果为1显示做多,如果为-1显示做空,0表示平仓
|
1116
|
+
"""
|
1117
|
+
js = ""
|
1118
|
+
if "trade_single" in data_item:
|
1119
|
+
js = f"""
|
1120
|
+
chart_option.series[{ii}].markPoint={{
|
1121
|
+
data:[
|
1122
|
+
"""
|
1123
|
+
trade_single = data_item["trade_single"]
|
1124
|
+
draw_df["row_number"] = draw_df.reset_index().index
|
1125
|
+
draw_df = draw_df[(draw_df[trade_single].notnull())]
|
1126
|
+
for index, row in draw_df.iterrows():
|
1127
|
+
idx = row["row_number"]
|
1128
|
+
dt_df_row = df.iloc[idx]
|
1129
|
+
dt_date = time_data[idx]
|
1130
|
+
# 计算Y轴坐标。如果是曲线,直接取值。如果是K线,取最高值
|
1131
|
+
if data_item["draw_type"] == "Kline":
|
1132
|
+
dt_data = row[data_item["col"][3]]
|
1133
|
+
else:
|
1134
|
+
dt_data = row[data_item["col"]]
|
1135
|
+
z = (
|
1136
|
+
"多"
|
1137
|
+
if row[trade_single] == 1
|
1138
|
+
else ("空" if row[trade_single] == -1 else "平")
|
1139
|
+
)
|
1140
|
+
color = (
|
1141
|
+
"red"
|
1142
|
+
if row[trade_single] == 1
|
1143
|
+
else ("green" if row[trade_single] == -1 else "yellow")
|
1144
|
+
)
|
1145
|
+
js += f"""
|
1146
|
+
{{
|
1147
|
+
coord:['{dt_date}',{dt_data}],
|
1148
|
+
label:{{
|
1149
|
+
position: 'bottom',
|
1150
|
+
verticalAlign: 'bottom',
|
1151
|
+
lineHeight: 56,
|
1152
|
+
normal: {{
|
1153
|
+
formatter: function (param) {{ return '{z}';}}
|
1154
|
+
}}
|
1155
|
+
}},
|
1156
|
+
symbol:'path://M 10,20 L 0,10 L 20,10 Z',
|
1157
|
+
symbolSize: 20,
|
1158
|
+
symbolOffset:[0, '-50%'],
|
1159
|
+
itemStyle: {{
|
1160
|
+
normal: {{color: '{color}'}}
|
1161
|
+
}}
|
1162
|
+
}},
|
1163
|
+
"""
|
1164
|
+
js += """
|
1165
|
+
]
|
1166
|
+
};
|
1167
|
+
"""
|
1168
|
+
return js
|
1169
|
+
|
1170
|
+
|
1171
|
+
def _produce_tooltip_formatter():
|
1172
|
+
js_code_str = """
|
1173
|
+
chart_option.tooltip.formatter = function(params) {
|
1174
|
+
let res = params[0].axisValue;
|
1175
|
+
for (let i = 0; i < params.length; i++) {
|
1176
|
+
// 判断 line_other_param[i] 是否存在以及 dec_length 是否存在
|
1177
|
+
let dec_length = (line_other_param[i] && line_other_param[i].dec_length) || 2; // 默认值为 2
|
1178
|
+
res += '<div>';
|
1179
|
+
res += params[i].marker;
|
1180
|
+
|
1181
|
+
if (params[i].seriesType === 'line') {
|
1182
|
+
res += params[i].seriesName + ':' + fixDecLen(params[i].value[1], dec_length);
|
1183
|
+
} else if (params[i].seriesType === 'candlestick') {
|
1184
|
+
let zdf = params[i].value[2] / params[i].value[1] - 1;
|
1185
|
+
zdf = (zdf * 100).toFixed(2);
|
1186
|
+
res += params[i].seriesName + ':开(' + fixDecLen(params[i].value[1], dec_length) + '), 收('
|
1187
|
+
+ fixDecLen(params[i].value[2], dec_length) + '), 低('
|
1188
|
+
+ fixDecLen(params[i].value[3], dec_length) + '), 高('
|
1189
|
+
+ fixDecLen(params[i].value[4], dec_length) + '), 涨(<span style="color:' + (zdf > 0 ? 'red' : 'green') + ';">'
|
1190
|
+
+ zdf + '%</span>)';
|
1191
|
+
} else {
|
1192
|
+
res += params[i].seriesName + ':' + fixDecLen(params[i].value, dec_length);
|
1193
|
+
}
|
1194
|
+
res += '</div>';
|
1195
|
+
}
|
1196
|
+
return res;
|
1197
|
+
}
|
1198
|
+
"""
|
1199
|
+
return js_code_str
|