libs.commands.report.results_report
libs/commands/report/results_report.py
1""" 2libs/commands/report/results_report.py 3""" 4 5import logging 6import os 7from datetime import datetime 8from io import BytesIO 9 10import matplotlib.font_manager as fm 11import matplotlib.pyplot as plt 12import pandas as pd 13from reportlab.lib import colors 14from reportlab.lib.enums import TA_LEFT, TA_RIGHT 15from reportlab.lib.pagesizes import A4, landscape 16from reportlab.lib.styles import ParagraphStyle 17from reportlab.lib.units import mm 18from reportlab.pdfbase import pdfmetrics 19from reportlab.pdfbase.ttfonts import TTFont 20from reportlab.platypus import (Image, LongTable, PageBreak, Paragraph, 21 SimpleDocTemplate, Spacer, TableStyle) 22 23import libs.global_value as g 24from libs.data import loader, lookup 25from libs.utils import dbutil, formatter 26 27 28def get_game_results() -> list: 29 """月/年単位のゲーム結果集計 30 31 Returns: 32 list: 集計結果のリスト 33 """ 34 35 g.params.update(starttime=g.params["starttime"].format("sql")) 36 g.params.update(endtime=g.params["endtime"].format("sql")) 37 38 resultdb = dbutil.get_connection() 39 rows = resultdb.execute( 40 loader.query_modification(loader.load_query("report/personal_data.sql")), 41 g.params, 42 ) 43 44 # --- データ収集 45 results = [ 46 [ 47 "", 48 "ゲーム数", 49 "通算\nポイント", 50 "平均\nポイント", 51 "1位", "", 52 "2位", "", 53 "3位", "", 54 "4位", "", 55 "平均\n順位", 56 "トビ", "", 57 ] 58 ] 59 60 for row in rows.fetchall(): 61 if row["ゲーム数"] == 0: 62 break 63 64 results.append( 65 [ 66 row["集計"], 67 row["ゲーム数"], 68 str(row["通算ポイント"]).replace("-", "▲") + "pt", 69 str(row["平均ポイント"]).replace("-", "▲") + "pt", 70 row["1位"], f"{row["1位率"]:.2f}%", 71 row["2位"], f"{row["2位率"]:.2f}%", 72 row["3位"], f"{row["3位率"]:.2f}%", 73 row["4位"], f"{row["4位率"]:.2f}%", 74 f"{row["平均順位"]:.2f}", 75 row["トビ"], f"{row["トビ率"]:.2f}%", 76 ] 77 ) 78 logging.info("return record: %s", len(results)) 79 resultdb.close() 80 81 if len(results) == 1: # ヘッダのみ 82 return [] 83 84 return results 85 86 87def get_count_results(game_count: int) -> list: 88 """指定間隔区切りのゲーム結果集計 89 90 Args: 91 game_count (int): 区切るゲーム数 92 93 Returns: 94 list: 集計結果のリスト 95 """ 96 97 g.params.update(interval=game_count) 98 resultdb = dbutil.get_connection() 99 rows = resultdb.execute( 100 loader.query_modification(loader.load_query("report/count_data.sql")), 101 g.params, 102 ) 103 104 # --- データ収集 105 results = [ 106 [ 107 "開始", 108 "終了", 109 "ゲーム数", 110 "通算\nポイント", 111 "平均\nポイント", 112 "1位", "", 113 "2位", "", 114 "3位", "", 115 "4位", "", 116 "平均\n順位", 117 "トビ", "", 118 ] 119 ] 120 121 for row in rows.fetchall(): 122 if row["ゲーム数"] == 0: 123 break 124 125 results.append( 126 [ 127 row["開始"], 128 row["終了"], 129 row["ゲーム数"], 130 str(row["通算ポイント"]).replace("-", "▲") + "pt", 131 str(row["平均ポイント"]).replace("-", "▲") + "pt", 132 row["1位"], f"{row["1位率"]:.2f}%", 133 row["2位"], f"{row["2位率"]:.2f}%", 134 row["3位"], f"{row["3位率"]:.2f}%", 135 row["4位"], f"{row["4位率"]:.2f}%", 136 f"{row["平均順位"]:.2f}", 137 row["トビ"], f"{row["トビ率"]:.2f}%", 138 ] 139 ) 140 logging.info("return record: %s", len(results)) 141 resultdb.close() 142 143 if len(results) == 1: # ヘッダのみ 144 return [] 145 146 return results 147 148 149def get_count_moving(game_count: int) -> list: 150 """移動平均を取得する 151 152 Args: 153 game_count (int): 平滑化するゲーム数 154 155 Returns: 156 list: 集計結果のリスト 157 """ 158 159 resultdb = dbutil.get_connection() 160 g.params.update(interval=game_count) 161 rows = resultdb.execute( 162 loader.query_modification(loader.load_query("report/count_moving.sql")), 163 g.params, 164 ) 165 166 # --- データ収集 167 results = [] 168 for row in rows.fetchall(): 169 results.append(dict(row)) 170 171 logging.info("return record: %s", len(results)) 172 resultdb.close() 173 174 return results 175 176 177def graphing_mean_rank(df: pd.DataFrame, title: str, whole: bool = False) -> BytesIO: 178 """平均順位の折れ線グラフを生成 179 180 Args: 181 df (pd.DataFrame): 描写データ 182 title (str): グラフタイトル 183 whole (bool, optional): 集計種別. Defaults to False. 184 - True: 全体集計 185 - False: 指定範囲集計 186 187 Returns: 188 BytesIO: 画像データ 189 """ 190 191 imgdata = BytesIO() 192 193 if whole: 194 df.plot( 195 kind="line", 196 figsize=(12, 5), 197 fontsize=14, 198 ) 199 plt.legend( 200 title="開始 - 終了", 201 ncol=int(len(df.columns) / 5) + 1, 202 ) 203 else: 204 df.plot( 205 kind="line", 206 y="rank_avg", 207 x="game_no", 208 legend=False, 209 figsize=(12, 5), 210 fontsize=14, 211 ) 212 213 plt.title(title, fontsize=18) 214 plt.grid(axis="y") 215 216 # Y軸設定 217 plt.ylabel("平均順位", fontsize=14) 218 plt.yticks([4.0, 3.5, 3.0, 2.5, 2.0, 1.5, 1.0]) 219 for ax in plt.gcf().get_axes(): # 逆向きにする 220 ax.invert_yaxis() 221 222 # X軸設定 223 plt.xlabel("ゲーム数", fontsize=14) 224 225 plt.savefig(imgdata, format="jpg", bbox_inches="tight") 226 plt.close() 227 228 return imgdata 229 230 231def graphing_total_points(df: pd.DataFrame, title: str, whole: bool = False) -> BytesIO: 232 """通算ポイント推移の折れ線グラフを生成 233 234 Args: 235 df (pd.DataFrame): 描写データ 236 title (str): グラフタイトル 237 whole (bool, optional): 集計種別. Defaults to False. 238 - True: 全体集計 / 移動平均付き 239 - False: 指定範囲集計 240 Returns: 241 BytesIO: 画像データ 242 """ 243 244 imgdata = BytesIO() 245 246 if whole: 247 df.plot( 248 kind="line", 249 figsize=(12, 8), 250 fontsize=14, 251 ) 252 plt.legend( 253 title="通算 ( 開始 - 終了 )", 254 ncol=int(len(df.columns) / 5) + 1, 255 ) 256 else: 257 point_sum = df.plot( 258 kind="line", 259 y="point_sum", 260 label="通算", 261 figsize=(12, 8), 262 fontsize=14, 263 ) 264 if len(df) > 50: 265 point_sum = df["point_sum"].rolling(40).mean().plot( 266 kind="line", label="移動平均(40ゲーム)", 267 ax=point_sum, 268 ) 269 if len(df) > 100: 270 point_sum = df["point_sum"].rolling(80).mean().plot( 271 kind="line", label="移動平均(80ゲーム)", 272 ax=point_sum, 273 ) 274 plt.legend() 275 276 plt.title(title, fontsize=18) 277 plt.grid(axis="y") 278 279 # Y軸設定 280 plt.ylabel("ポイント", fontsize=14) 281 ylocs, ylabs = plt.yticks() 282 new_ylabs = [ylab.get_text().replace("−", "▲") for ylab in ylabs] 283 plt.yticks(list(ylocs[1:-1]), new_ylabs[1:-1]) 284 285 # X軸設定 286 plt.xlabel("ゲーム数", fontsize=14) 287 288 plt.savefig(imgdata, format="jpg", bbox_inches="tight") 289 plt.close() 290 291 return imgdata 292 293 294def graphing_rank_distribution(df: pd.DataFrame, title: str) -> BytesIO: 295 """順位分布の棒グラフを生成 296 297 Args: 298 df (pd.DataFrame): 描写データ 299 title (str): グラフタイトル 300 301 Returns: 302 BytesIO: 画像データ 303 """ 304 305 imgdata = BytesIO() 306 307 df.plot( 308 kind="bar", 309 stacked=True, 310 figsize=(12, 7), 311 fontsize=14, 312 ) 313 314 plt.title(title, fontsize=18) 315 plt.legend( 316 bbox_to_anchor=(0.5, 0), 317 loc="lower center", 318 ncol=4, 319 fontsize=12, 320 ) 321 322 # Y軸設定 323 plt.yticks([0, 25, 50, 75, 100]) 324 plt.ylabel("(%)", fontsize=14) 325 for ax in plt.gcf().get_axes(): # グリッド線を背後にまわす 326 ax.set_axisbelow(True) 327 plt.grid(axis="y") 328 329 # X軸設定 330 if len(df) > 10: 331 plt.xticks(rotation=30, ha="right") 332 else: 333 plt.xticks(rotation=30) 334 335 plt.savefig(imgdata, format="jpg", bbox_inches="tight") 336 plt.close() 337 338 return imgdata 339 340 341def gen_pdf() -> tuple[str | bool, str | bool]: 342 """成績レポートを生成する 343 344 Returns: 345 tuple[str | bool, str | bool]: 346 - str: レポート対象メンバー名 347 - str: レポート保存パス 348 """ 349 350 plt.close() 351 352 if not g.params.get("player_name"): # レポート対象の指定なし 353 return (False, False) 354 355 # 対象メンバーの記録状況 356 target_info = lookup.db.member_info(g.params["player_name"]) 357 logging.info(target_info) 358 359 if not target_info["game_count"] > 0: # 記録なし 360 return (False, False) 361 362 # 書式設定 363 font_path = os.path.join(os.path.realpath(os.path.curdir), g.cfg.setting.font_file) 364 pdf_path = os.path.join( 365 g.cfg.setting.work_dir, 366 f"{g.params["filename"]}.pdf" if g.params.get("filename") else "results.pdf", 367 ) 368 pdfmetrics.registerFont(TTFont("ReportFont", font_path)) 369 370 doc = SimpleDocTemplate( 371 pdf_path, 372 pagesize=landscape(A4), 373 topMargin=10.0 * mm, 374 bottomMargin=10.0 * mm, 375 # leftMargin=1.5 * mm, 376 # rightMargin=1.5 * mm, 377 ) 378 379 style: dict = {} 380 style["Title"] = ParagraphStyle( 381 name="Title", fontName="ReportFont", fontSize=24 382 ) 383 style["Normal"] = ParagraphStyle( 384 name="Normal", fontName="ReportFont", fontSize=14 385 ) 386 style["Left"] = ParagraphStyle( 387 name="Left", fontName="ReportFont", fontSize=14, alignment=TA_LEFT 388 ) 389 style["Right"] = ParagraphStyle( 390 name="Right", fontName="ReportFont", fontSize=14, alignment=TA_RIGHT 391 ) 392 393 plt.rcParams.update(plt.rcParamsDefault) 394 font_prop = fm.FontProperties(fname=font_path) 395 plt.rcParams["font.family"] = font_prop.get_name() 396 fm.fontManager.addfont(font_path) 397 398 # レポート作成 399 elements: list = [] 400 elements.extend(cover_page(style, target_info)) # 表紙 401 elements.extend(entire_aggregate(style)) # 全期間 402 elements.extend(periodic_aggregation(style)) # 期間集計 403 elements.extend(sectional_aggregate(style, target_info)) # 区間集計 404 405 doc.build(elements) 406 logging.notice("report generation: %s", g.params["player_name"]) # type: ignore 407 408 return (g.params["player_name"], pdf_path) 409 410 411def cover_page(style: dict, target_info: dict) -> list: 412 """表紙生成 413 414 Args: 415 style (dict): レイアウトスタイル 416 target_info (dict): プレイヤー情報 417 418 Returns: 419 list: 生成内容 420 """ 421 422 elements: list = [] 423 424 first_game = datetime.fromtimestamp( # 最初のゲーム日時 425 float(target_info["first_game"]) 426 ) 427 last_game = datetime.fromtimestamp( # 最後のゲーム日時 428 float(target_info["last_game"]) 429 ) 430 431 if g.params.get("anonymous"): 432 mapping_dict = formatter.anonymous_mapping([g.params["player_name"]]) 433 target_player = next(iter(mapping_dict.values())) 434 else: 435 target_player = g.params["player_name"] 436 437 # 表紙 438 elements.append(Spacer(1, 40 * mm)) 439 elements.append(Paragraph(f"成績レポート:{target_player}", style["Title"])) 440 elements.append(Spacer(1, 10 * mm)) 441 elements.append( 442 Paragraph( 443 f"集計期間:{first_game.strftime("%Y-%m-%d %H:%M")} - {last_game.strftime("%Y-%m-%d %H:%M")}", 444 style["Normal"] 445 ) 446 ) 447 elements.append(Spacer(1, 100 * mm)) 448 elements.append( 449 Paragraph( 450 f"作成日:{datetime.now().strftime('%Y-%m-%d')}", 451 style["Right"] 452 ) 453 ) 454 elements.append(PageBreak()) 455 456 return elements 457 458 459def entire_aggregate(style: dict) -> list: 460 """全期間 461 462 Args: 463 style (dict): レイアウトスタイル 464 465 Returns: 466 list: 生成内容 467 """ 468 469 elements: list = [] 470 471 elements.append(Paragraph("全期間", style["Left"])) 472 elements.append(Spacer(1, 5 * mm)) 473 data: list = [] 474 g.cfg.aggregate_unit = "A" 475 tmp_data = get_game_results() 476 477 if not tmp_data: 478 return [] 479 480 for _, val in enumerate(tmp_data): # ゲーム数を除外 481 data.append(val[1:]) 482 tt = LongTable(data, repeatRows=1) 483 tt.setStyle(TableStyle([ 484 ("FONT", (0, 0), (-1, -1), "ReportFont", 10), 485 ("GRID", (0, 0), (-1, -1), 0.25, colors.black), 486 ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), 487 ("ALIGN", (0, 0), (-1, -1), "CENTER"), 488 ('SPAN', (3, 0), (4, 0)), 489 ('SPAN', (5, 0), (6, 0)), 490 ('SPAN', (7, 0), (8, 0)), 491 ('SPAN', (9, 0), (10, 0)), 492 ('SPAN', (12, 0), (13, 0)), 493 # ヘッダ行 494 ("BACKGROUND", (0, 0), (-1, 0), colors.navy), 495 ("TEXTCOLOR", (0, 0), (-1, 0), colors.white), 496 ])) 497 elements.append(tt) 498 499 # 順位分布 500 imgdata = BytesIO() 501 gdata = pd.DataFrame( 502 { 503 "順位分布": [ 504 float(str(data[1][4]).replace("%", "")), 505 float(str(data[1][6]).replace("%", "")), 506 float(str(data[1][8]).replace("%", "")), 507 float(str(data[1][10]).replace("%", "")), 508 ], 509 }, index=["1位率", "2位率", "3位率", "4位率"] 510 ) 511 gdata.plot( 512 kind="pie", 513 y="順位分布", 514 labels=None, 515 figsize=(6, 6), 516 fontsize=14, 517 autopct="%.2f%%", 518 wedgeprops={"linewidth": 1, "edgecolor": "white"}, 519 ) 520 plt.title("順位分布 ( 全期間 )", fontsize=18) 521 plt.ylabel("") 522 plt.legend( 523 list(gdata.index), 524 bbox_to_anchor=(0.5, -0.1), 525 loc="lower center", 526 ncol=4, 527 fontsize=12 528 ) 529 plt.savefig(imgdata, format="jpg", bbox_inches="tight") 530 531 elements.append(Spacer(1, 5 * mm)) 532 elements.append(Image(imgdata, width=600 * 0.5, height=600 * 0.5)) 533 plt.close() 534 535 data = get_count_moving(0) 536 df = pd.DataFrame(data) 537 df["playtime"] = pd.to_datetime(df["playtime"]) 538 539 # 通算ポイント推移 540 imgdata = graphing_total_points(df, "通算ポイント推移 ( 全期間 )", False) 541 elements.append(Image(imgdata, width=1200 * 0.5, height=800 * 0.5)) 542 543 # 平均順位 544 imgdata = graphing_mean_rank(df, "平均順位推移 ( 全期間 )", False) 545 elements.append(Image(imgdata, width=1200 * 0.5, height=500 * 0.5)) 546 547 elements.append(PageBreak()) 548 549 return elements 550 551 552def periodic_aggregation(style: dict) -> list: 553 """期間集計 554 555 Args: 556 style (dict): レイアウトスタイル 557 558 Returns: 559 list: 生成内容 560 """ 561 562 elements: list = [] 563 564 pattern: list[tuple[str, str, str]] = [ 565 # 表タイトル, グラフタイトル, フラグ 566 ("月別集計", "順位分布(月別)", "M"), 567 ("年別集計", "順位分布(年別)", "Y"), 568 ] 569 570 for table_title, graph_title, flag in pattern: 571 elements.append(Paragraph(table_title, style["Left"])) 572 elements.append(Spacer(1, 5 * mm)) 573 574 data: list = [] 575 g.cfg.aggregate_unit = flag 576 tmp_data = get_game_results() 577 578 if not tmp_data: 579 return [] 580 581 for _, val in enumerate(tmp_data): # 日時を除外 582 data.append(val[:15]) 583 584 tt = LongTable(data, repeatRows=1) 585 ts = TableStyle([ 586 ("FONT", (0, 0), (-1, -1), "ReportFont", 10), 587 ("GRID", (0, 0), (-1, -1), 0.25, colors.black), 588 ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), 589 ("ALIGN", (0, 0), (-1, -1), "CENTER"), 590 ('SPAN', (4, 0), (5, 0)), 591 ('SPAN', (6, 0), (7, 0)), 592 ('SPAN', (8, 0), (9, 0)), 593 ('SPAN', (10, 0), (11, 0)), 594 ('SPAN', (13, 0), (14, 0)), 595 # ヘッダ行 596 ("BACKGROUND", (0, 0), (-1, 0), colors.navy), 597 ("TEXTCOLOR", (0, 0), (-1, 0), colors.white), 598 ]) 599 600 if len(data) > 4: 601 for i in range(len(data) - 2): 602 if i % 2 == 0: 603 ts.add("BACKGROUND", (0, i + 2), (-1, i + 2), colors.lightgrey) 604 tt.setStyle(ts) 605 elements.append(tt) 606 elements.append(Spacer(1, 10 * mm)) 607 608 # 順位分布 609 df = pd.DataFrame( 610 { 611 "1位率": [float(str(data[x + 1][5]).replace("%", "")) for x in range(len(data) - 1)], 612 "2位率": [float(str(data[x + 1][7]).replace("%", "")) for x in range(len(data) - 1)], 613 "3位率": [float(str(data[x + 1][9]).replace("%", "")) for x in range(len(data) - 1)], 614 "4位率": [float(str(data[x + 1][11]).replace("%", "")) for x in range(len(data) - 1)], 615 }, index=[data[x + 1][0] for x in range(len(data) - 1)] 616 ) 617 618 imgdata = graphing_rank_distribution(df, graph_title) 619 elements.append(Spacer(1, 5 * mm)) 620 elements.append(Image(imgdata, width=1200 * 0.5, height=700 * 0.5)) 621 622 elements.append(PageBreak()) 623 624 return elements 625 626 627def sectional_aggregate(style: dict, target_info: dict) -> list: 628 """区間集計 629 630 Args: 631 style (dict): レイアウトスタイル 632 target_info (dict): プレイヤー情報 633 634 Returns: 635 list: 生成内容 636 """ 637 638 elements: list = [] 639 640 pattern: list[tuple[int, int, str]] = [ 641 # 区切り回数, 閾値, タイトル 642 (80, 100, "短期"), 643 (200, 240, "中期"), 644 (400, 500, "長期"), 645 ] 646 647 for count, threshold, title in pattern: 648 if target_info["game_count"] > threshold: 649 # テーブル 650 elements.append(Paragraph(f"区間集計 ( {title} )", style["Left"])) 651 elements.append(Spacer(1, 5 * mm)) 652 data = get_count_results(count) 653 654 if not data: 655 return [] 656 657 tt = LongTable(data, repeatRows=1) 658 ts = TableStyle([ 659 ("FONT", (0, 0), (-1, -1), "ReportFont", 10), 660 ("GRID", (0, 0), (-1, -1), 0.25, colors.black), 661 ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), 662 ("ALIGN", (0, 0), (-1, -1), "CENTER"), 663 ('SPAN', (5, 0), (6, 0)), 664 ('SPAN', (7, 0), (8, 0)), 665 ('SPAN', (9, 0), (10, 0)), 666 ('SPAN', (11, 0), (12, 0)), 667 ('SPAN', (14, 0), (15, 0)), 668 # ヘッダ行 669 ("BACKGROUND", (0, 0), (-1, 0), colors.navy), 670 ("TEXTCOLOR", (0, 0), (-1, 0), colors.white), 671 ]) 672 if len(data) > 4: 673 for i in range(len(data) - 2): 674 if i % 2 == 0: 675 ts.add("BACKGROUND", (0, i + 2), (-1, i + 2), colors.lightgrey) 676 tt.setStyle(ts) 677 elements.append(tt) 678 679 # 順位分布 680 df = pd.DataFrame( 681 { 682 "1位率": [float(str(data[x + 1][6]).replace("%", "")) for x in range(len(data) - 1)], 683 "2位率": [float(str(data[x + 1][8]).replace("%", "")) for x in range(len(data) - 1)], 684 "3位率": [float(str(data[x + 1][10]).replace("%", "")) for x in range(len(data) - 1)], 685 "4位率": [float(str(data[x + 1][12]).replace("%", "")) for x in range(len(data) - 1)], 686 }, index=[f"{str(data[x + 1][0])} - {str(data[x + 1][1])}" for x in range(len(data) - 1)] 687 ) 688 689 imgdata = graphing_rank_distribution(df, f"順位分布 ( 区間 {title} )") 690 elements.append(Spacer(1, 5 * mm)) 691 elements.append(Image(imgdata, width=1200 * 0.5, height=800 * 0.5)) 692 693 # 通算ポイント推移 694 data = get_count_moving(count) 695 tmp_df = pd.DataFrame(data) 696 df = pd.DataFrame() 697 for i in sorted(tmp_df["interval"].unique().tolist()): 698 list_data = tmp_df[tmp_df.interval == i]["point_sum"].to_list() 699 game_count = tmp_df[tmp_df.interval == i]["total_count"].to_list() 700 df[f"{min(game_count)} - {max(game_count)}"] = [None] * (count - len(list_data)) + list_data 701 702 imgdata = graphing_total_points(df, f"通算ポイント推移(区間 {title})", True) 703 elements.append(Image(imgdata, width=1200 * 0.5, height=800 * 0.5)) 704 705 # 平均順位 706 df = pd.DataFrame() 707 for i in sorted(tmp_df["interval"].unique().tolist()): 708 list_data = tmp_df[tmp_df.interval == i]["rank_avg"].to_list() 709 game_count = tmp_df[tmp_df.interval == i]["total_count"].to_list() 710 df[f"{min(game_count)} - {max(game_count)}"] = [None] * (count - len(list_data)) + list_data 711 712 imgdata = graphing_mean_rank(df, f"平均順位推移(区間 {title})", True) 713 elements.append(Image(imgdata, width=1200 * 0.5, height=500 * 0.5)) 714 715 elements.append(PageBreak()) 716 717 return elements
def
get_game_results() -> list:
29def get_game_results() -> list: 30 """月/年単位のゲーム結果集計 31 32 Returns: 33 list: 集計結果のリスト 34 """ 35 36 g.params.update(starttime=g.params["starttime"].format("sql")) 37 g.params.update(endtime=g.params["endtime"].format("sql")) 38 39 resultdb = dbutil.get_connection() 40 rows = resultdb.execute( 41 loader.query_modification(loader.load_query("report/personal_data.sql")), 42 g.params, 43 ) 44 45 # --- データ収集 46 results = [ 47 [ 48 "", 49 "ゲーム数", 50 "通算\nポイント", 51 "平均\nポイント", 52 "1位", "", 53 "2位", "", 54 "3位", "", 55 "4位", "", 56 "平均\n順位", 57 "トビ", "", 58 ] 59 ] 60 61 for row in rows.fetchall(): 62 if row["ゲーム数"] == 0: 63 break 64 65 results.append( 66 [ 67 row["集計"], 68 row["ゲーム数"], 69 str(row["通算ポイント"]).replace("-", "▲") + "pt", 70 str(row["平均ポイント"]).replace("-", "▲") + "pt", 71 row["1位"], f"{row["1位率"]:.2f}%", 72 row["2位"], f"{row["2位率"]:.2f}%", 73 row["3位"], f"{row["3位率"]:.2f}%", 74 row["4位"], f"{row["4位率"]:.2f}%", 75 f"{row["平均順位"]:.2f}", 76 row["トビ"], f"{row["トビ率"]:.2f}%", 77 ] 78 ) 79 logging.info("return record: %s", len(results)) 80 resultdb.close() 81 82 if len(results) == 1: # ヘッダのみ 83 return [] 84 85 return results
月/年単位のゲーム結果集計
Returns:
list: 集計結果のリスト
def
get_count_results(game_count: int) -> list:
88def get_count_results(game_count: int) -> list: 89 """指定間隔区切りのゲーム結果集計 90 91 Args: 92 game_count (int): 区切るゲーム数 93 94 Returns: 95 list: 集計結果のリスト 96 """ 97 98 g.params.update(interval=game_count) 99 resultdb = dbutil.get_connection() 100 rows = resultdb.execute( 101 loader.query_modification(loader.load_query("report/count_data.sql")), 102 g.params, 103 ) 104 105 # --- データ収集 106 results = [ 107 [ 108 "開始", 109 "終了", 110 "ゲーム数", 111 "通算\nポイント", 112 "平均\nポイント", 113 "1位", "", 114 "2位", "", 115 "3位", "", 116 "4位", "", 117 "平均\n順位", 118 "トビ", "", 119 ] 120 ] 121 122 for row in rows.fetchall(): 123 if row["ゲーム数"] == 0: 124 break 125 126 results.append( 127 [ 128 row["開始"], 129 row["終了"], 130 row["ゲーム数"], 131 str(row["通算ポイント"]).replace("-", "▲") + "pt", 132 str(row["平均ポイント"]).replace("-", "▲") + "pt", 133 row["1位"], f"{row["1位率"]:.2f}%", 134 row["2位"], f"{row["2位率"]:.2f}%", 135 row["3位"], f"{row["3位率"]:.2f}%", 136 row["4位"], f"{row["4位率"]:.2f}%", 137 f"{row["平均順位"]:.2f}", 138 row["トビ"], f"{row["トビ率"]:.2f}%", 139 ] 140 ) 141 logging.info("return record: %s", len(results)) 142 resultdb.close() 143 144 if len(results) == 1: # ヘッダのみ 145 return [] 146 147 return results
指定間隔区切りのゲーム結果集計
Arguments:
- game_count (int): 区切るゲーム数
Returns:
list: 集計結果のリスト
def
get_count_moving(game_count: int) -> list:
150def get_count_moving(game_count: int) -> list: 151 """移動平均を取得する 152 153 Args: 154 game_count (int): 平滑化するゲーム数 155 156 Returns: 157 list: 集計結果のリスト 158 """ 159 160 resultdb = dbutil.get_connection() 161 g.params.update(interval=game_count) 162 rows = resultdb.execute( 163 loader.query_modification(loader.load_query("report/count_moving.sql")), 164 g.params, 165 ) 166 167 # --- データ収集 168 results = [] 169 for row in rows.fetchall(): 170 results.append(dict(row)) 171 172 logging.info("return record: %s", len(results)) 173 resultdb.close() 174 175 return results
移動平均を取得する
Arguments:
- game_count (int): 平滑化するゲーム数
Returns:
list: 集計結果のリスト
def
graphing_mean_rank( df: pandas.core.frame.DataFrame, title: str, whole: bool = False) -> _io.BytesIO:
178def graphing_mean_rank(df: pd.DataFrame, title: str, whole: bool = False) -> BytesIO: 179 """平均順位の折れ線グラフを生成 180 181 Args: 182 df (pd.DataFrame): 描写データ 183 title (str): グラフタイトル 184 whole (bool, optional): 集計種別. Defaults to False. 185 - True: 全体集計 186 - False: 指定範囲集計 187 188 Returns: 189 BytesIO: 画像データ 190 """ 191 192 imgdata = BytesIO() 193 194 if whole: 195 df.plot( 196 kind="line", 197 figsize=(12, 5), 198 fontsize=14, 199 ) 200 plt.legend( 201 title="開始 - 終了", 202 ncol=int(len(df.columns) / 5) + 1, 203 ) 204 else: 205 df.plot( 206 kind="line", 207 y="rank_avg", 208 x="game_no", 209 legend=False, 210 figsize=(12, 5), 211 fontsize=14, 212 ) 213 214 plt.title(title, fontsize=18) 215 plt.grid(axis="y") 216 217 # Y軸設定 218 plt.ylabel("平均順位", fontsize=14) 219 plt.yticks([4.0, 3.5, 3.0, 2.5, 2.0, 1.5, 1.0]) 220 for ax in plt.gcf().get_axes(): # 逆向きにする 221 ax.invert_yaxis() 222 223 # X軸設定 224 plt.xlabel("ゲーム数", fontsize=14) 225 226 plt.savefig(imgdata, format="jpg", bbox_inches="tight") 227 plt.close() 228 229 return imgdata
平均順位の折れ線グラフを生成
Arguments:
- df (pd.DataFrame): 描写データ
- title (str): グラフタイトル
- whole (bool, optional): 集計種別. Defaults to False.
- True: 全体集計
- False: 指定範囲集計
Returns:
BytesIO: 画像データ
def
graphing_total_points( df: pandas.core.frame.DataFrame, title: str, whole: bool = False) -> _io.BytesIO:
232def graphing_total_points(df: pd.DataFrame, title: str, whole: bool = False) -> BytesIO: 233 """通算ポイント推移の折れ線グラフを生成 234 235 Args: 236 df (pd.DataFrame): 描写データ 237 title (str): グラフタイトル 238 whole (bool, optional): 集計種別. Defaults to False. 239 - True: 全体集計 / 移動平均付き 240 - False: 指定範囲集計 241 Returns: 242 BytesIO: 画像データ 243 """ 244 245 imgdata = BytesIO() 246 247 if whole: 248 df.plot( 249 kind="line", 250 figsize=(12, 8), 251 fontsize=14, 252 ) 253 plt.legend( 254 title="通算 ( 開始 - 終了 )", 255 ncol=int(len(df.columns) / 5) + 1, 256 ) 257 else: 258 point_sum = df.plot( 259 kind="line", 260 y="point_sum", 261 label="通算", 262 figsize=(12, 8), 263 fontsize=14, 264 ) 265 if len(df) > 50: 266 point_sum = df["point_sum"].rolling(40).mean().plot( 267 kind="line", label="移動平均(40ゲーム)", 268 ax=point_sum, 269 ) 270 if len(df) > 100: 271 point_sum = df["point_sum"].rolling(80).mean().plot( 272 kind="line", label="移動平均(80ゲーム)", 273 ax=point_sum, 274 ) 275 plt.legend() 276 277 plt.title(title, fontsize=18) 278 plt.grid(axis="y") 279 280 # Y軸設定 281 plt.ylabel("ポイント", fontsize=14) 282 ylocs, ylabs = plt.yticks() 283 new_ylabs = [ylab.get_text().replace("−", "▲") for ylab in ylabs] 284 plt.yticks(list(ylocs[1:-1]), new_ylabs[1:-1]) 285 286 # X軸設定 287 plt.xlabel("ゲーム数", fontsize=14) 288 289 plt.savefig(imgdata, format="jpg", bbox_inches="tight") 290 plt.close() 291 292 return imgdata
通算ポイント推移の折れ線グラフを生成
Arguments:
- df (pd.DataFrame): 描写データ
- title (str): グラフタイトル
- whole (bool, optional): 集計種別. Defaults to False.
- True: 全体集計 / 移動平均付き
- False: 指定範囲集計
Returns:
BytesIO: 画像データ
def
graphing_rank_distribution(df: pandas.core.frame.DataFrame, title: str) -> _io.BytesIO:
295def graphing_rank_distribution(df: pd.DataFrame, title: str) -> BytesIO: 296 """順位分布の棒グラフを生成 297 298 Args: 299 df (pd.DataFrame): 描写データ 300 title (str): グラフタイトル 301 302 Returns: 303 BytesIO: 画像データ 304 """ 305 306 imgdata = BytesIO() 307 308 df.plot( 309 kind="bar", 310 stacked=True, 311 figsize=(12, 7), 312 fontsize=14, 313 ) 314 315 plt.title(title, fontsize=18) 316 plt.legend( 317 bbox_to_anchor=(0.5, 0), 318 loc="lower center", 319 ncol=4, 320 fontsize=12, 321 ) 322 323 # Y軸設定 324 plt.yticks([0, 25, 50, 75, 100]) 325 plt.ylabel("(%)", fontsize=14) 326 for ax in plt.gcf().get_axes(): # グリッド線を背後にまわす 327 ax.set_axisbelow(True) 328 plt.grid(axis="y") 329 330 # X軸設定 331 if len(df) > 10: 332 plt.xticks(rotation=30, ha="right") 333 else: 334 plt.xticks(rotation=30) 335 336 plt.savefig(imgdata, format="jpg", bbox_inches="tight") 337 plt.close() 338 339 return imgdata
順位分布の棒グラフを生成
Arguments:
- df (pd.DataFrame): 描写データ
- title (str): グラフタイトル
Returns:
BytesIO: 画像データ
def
gen_pdf() -> tuple[str | bool, str | bool]:
342def gen_pdf() -> tuple[str | bool, str | bool]: 343 """成績レポートを生成する 344 345 Returns: 346 tuple[str | bool, str | bool]: 347 - str: レポート対象メンバー名 348 - str: レポート保存パス 349 """ 350 351 plt.close() 352 353 if not g.params.get("player_name"): # レポート対象の指定なし 354 return (False, False) 355 356 # 対象メンバーの記録状況 357 target_info = lookup.db.member_info(g.params["player_name"]) 358 logging.info(target_info) 359 360 if not target_info["game_count"] > 0: # 記録なし 361 return (False, False) 362 363 # 書式設定 364 font_path = os.path.join(os.path.realpath(os.path.curdir), g.cfg.setting.font_file) 365 pdf_path = os.path.join( 366 g.cfg.setting.work_dir, 367 f"{g.params["filename"]}.pdf" if g.params.get("filename") else "results.pdf", 368 ) 369 pdfmetrics.registerFont(TTFont("ReportFont", font_path)) 370 371 doc = SimpleDocTemplate( 372 pdf_path, 373 pagesize=landscape(A4), 374 topMargin=10.0 * mm, 375 bottomMargin=10.0 * mm, 376 # leftMargin=1.5 * mm, 377 # rightMargin=1.5 * mm, 378 ) 379 380 style: dict = {} 381 style["Title"] = ParagraphStyle( 382 name="Title", fontName="ReportFont", fontSize=24 383 ) 384 style["Normal"] = ParagraphStyle( 385 name="Normal", fontName="ReportFont", fontSize=14 386 ) 387 style["Left"] = ParagraphStyle( 388 name="Left", fontName="ReportFont", fontSize=14, alignment=TA_LEFT 389 ) 390 style["Right"] = ParagraphStyle( 391 name="Right", fontName="ReportFont", fontSize=14, alignment=TA_RIGHT 392 ) 393 394 plt.rcParams.update(plt.rcParamsDefault) 395 font_prop = fm.FontProperties(fname=font_path) 396 plt.rcParams["font.family"] = font_prop.get_name() 397 fm.fontManager.addfont(font_path) 398 399 # レポート作成 400 elements: list = [] 401 elements.extend(cover_page(style, target_info)) # 表紙 402 elements.extend(entire_aggregate(style)) # 全期間 403 elements.extend(periodic_aggregation(style)) # 期間集計 404 elements.extend(sectional_aggregate(style, target_info)) # 区間集計 405 406 doc.build(elements) 407 logging.notice("report generation: %s", g.params["player_name"]) # type: ignore 408 409 return (g.params["player_name"], pdf_path)
成績レポートを生成する
Returns:
tuple[str | bool, str | bool]:
- str: レポート対象メンバー名
- str: レポート保存パス
def
cover_page(style: dict, target_info: dict) -> list:
412def cover_page(style: dict, target_info: dict) -> list: 413 """表紙生成 414 415 Args: 416 style (dict): レイアウトスタイル 417 target_info (dict): プレイヤー情報 418 419 Returns: 420 list: 生成内容 421 """ 422 423 elements: list = [] 424 425 first_game = datetime.fromtimestamp( # 最初のゲーム日時 426 float(target_info["first_game"]) 427 ) 428 last_game = datetime.fromtimestamp( # 最後のゲーム日時 429 float(target_info["last_game"]) 430 ) 431 432 if g.params.get("anonymous"): 433 mapping_dict = formatter.anonymous_mapping([g.params["player_name"]]) 434 target_player = next(iter(mapping_dict.values())) 435 else: 436 target_player = g.params["player_name"] 437 438 # 表紙 439 elements.append(Spacer(1, 40 * mm)) 440 elements.append(Paragraph(f"成績レポート:{target_player}", style["Title"])) 441 elements.append(Spacer(1, 10 * mm)) 442 elements.append( 443 Paragraph( 444 f"集計期間:{first_game.strftime("%Y-%m-%d %H:%M")} - {last_game.strftime("%Y-%m-%d %H:%M")}", 445 style["Normal"] 446 ) 447 ) 448 elements.append(Spacer(1, 100 * mm)) 449 elements.append( 450 Paragraph( 451 f"作成日:{datetime.now().strftime('%Y-%m-%d')}", 452 style["Right"] 453 ) 454 ) 455 elements.append(PageBreak()) 456 457 return elements
表紙生成
Arguments:
- style (dict): レイアウトスタイル
- target_info (dict): プレイヤー情報
Returns:
list: 生成内容
def
entire_aggregate(style: dict) -> list:
460def entire_aggregate(style: dict) -> list: 461 """全期間 462 463 Args: 464 style (dict): レイアウトスタイル 465 466 Returns: 467 list: 生成内容 468 """ 469 470 elements: list = [] 471 472 elements.append(Paragraph("全期間", style["Left"])) 473 elements.append(Spacer(1, 5 * mm)) 474 data: list = [] 475 g.cfg.aggregate_unit = "A" 476 tmp_data = get_game_results() 477 478 if not tmp_data: 479 return [] 480 481 for _, val in enumerate(tmp_data): # ゲーム数を除外 482 data.append(val[1:]) 483 tt = LongTable(data, repeatRows=1) 484 tt.setStyle(TableStyle([ 485 ("FONT", (0, 0), (-1, -1), "ReportFont", 10), 486 ("GRID", (0, 0), (-1, -1), 0.25, colors.black), 487 ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), 488 ("ALIGN", (0, 0), (-1, -1), "CENTER"), 489 ('SPAN', (3, 0), (4, 0)), 490 ('SPAN', (5, 0), (6, 0)), 491 ('SPAN', (7, 0), (8, 0)), 492 ('SPAN', (9, 0), (10, 0)), 493 ('SPAN', (12, 0), (13, 0)), 494 # ヘッダ行 495 ("BACKGROUND", (0, 0), (-1, 0), colors.navy), 496 ("TEXTCOLOR", (0, 0), (-1, 0), colors.white), 497 ])) 498 elements.append(tt) 499 500 # 順位分布 501 imgdata = BytesIO() 502 gdata = pd.DataFrame( 503 { 504 "順位分布": [ 505 float(str(data[1][4]).replace("%", "")), 506 float(str(data[1][6]).replace("%", "")), 507 float(str(data[1][8]).replace("%", "")), 508 float(str(data[1][10]).replace("%", "")), 509 ], 510 }, index=["1位率", "2位率", "3位率", "4位率"] 511 ) 512 gdata.plot( 513 kind="pie", 514 y="順位分布", 515 labels=None, 516 figsize=(6, 6), 517 fontsize=14, 518 autopct="%.2f%%", 519 wedgeprops={"linewidth": 1, "edgecolor": "white"}, 520 ) 521 plt.title("順位分布 ( 全期間 )", fontsize=18) 522 plt.ylabel("") 523 plt.legend( 524 list(gdata.index), 525 bbox_to_anchor=(0.5, -0.1), 526 loc="lower center", 527 ncol=4, 528 fontsize=12 529 ) 530 plt.savefig(imgdata, format="jpg", bbox_inches="tight") 531 532 elements.append(Spacer(1, 5 * mm)) 533 elements.append(Image(imgdata, width=600 * 0.5, height=600 * 0.5)) 534 plt.close() 535 536 data = get_count_moving(0) 537 df = pd.DataFrame(data) 538 df["playtime"] = pd.to_datetime(df["playtime"]) 539 540 # 通算ポイント推移 541 imgdata = graphing_total_points(df, "通算ポイント推移 ( 全期間 )", False) 542 elements.append(Image(imgdata, width=1200 * 0.5, height=800 * 0.5)) 543 544 # 平均順位 545 imgdata = graphing_mean_rank(df, "平均順位推移 ( 全期間 )", False) 546 elements.append(Image(imgdata, width=1200 * 0.5, height=500 * 0.5)) 547 548 elements.append(PageBreak()) 549 550 return elements
全期間
Arguments:
- style (dict): レイアウトスタイル
Returns:
list: 生成内容
def
periodic_aggregation(style: dict) -> list:
553def periodic_aggregation(style: dict) -> list: 554 """期間集計 555 556 Args: 557 style (dict): レイアウトスタイル 558 559 Returns: 560 list: 生成内容 561 """ 562 563 elements: list = [] 564 565 pattern: list[tuple[str, str, str]] = [ 566 # 表タイトル, グラフタイトル, フラグ 567 ("月別集計", "順位分布(月別)", "M"), 568 ("年別集計", "順位分布(年別)", "Y"), 569 ] 570 571 for table_title, graph_title, flag in pattern: 572 elements.append(Paragraph(table_title, style["Left"])) 573 elements.append(Spacer(1, 5 * mm)) 574 575 data: list = [] 576 g.cfg.aggregate_unit = flag 577 tmp_data = get_game_results() 578 579 if not tmp_data: 580 return [] 581 582 for _, val in enumerate(tmp_data): # 日時を除外 583 data.append(val[:15]) 584 585 tt = LongTable(data, repeatRows=1) 586 ts = TableStyle([ 587 ("FONT", (0, 0), (-1, -1), "ReportFont", 10), 588 ("GRID", (0, 0), (-1, -1), 0.25, colors.black), 589 ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), 590 ("ALIGN", (0, 0), (-1, -1), "CENTER"), 591 ('SPAN', (4, 0), (5, 0)), 592 ('SPAN', (6, 0), (7, 0)), 593 ('SPAN', (8, 0), (9, 0)), 594 ('SPAN', (10, 0), (11, 0)), 595 ('SPAN', (13, 0), (14, 0)), 596 # ヘッダ行 597 ("BACKGROUND", (0, 0), (-1, 0), colors.navy), 598 ("TEXTCOLOR", (0, 0), (-1, 0), colors.white), 599 ]) 600 601 if len(data) > 4: 602 for i in range(len(data) - 2): 603 if i % 2 == 0: 604 ts.add("BACKGROUND", (0, i + 2), (-1, i + 2), colors.lightgrey) 605 tt.setStyle(ts) 606 elements.append(tt) 607 elements.append(Spacer(1, 10 * mm)) 608 609 # 順位分布 610 df = pd.DataFrame( 611 { 612 "1位率": [float(str(data[x + 1][5]).replace("%", "")) for x in range(len(data) - 1)], 613 "2位率": [float(str(data[x + 1][7]).replace("%", "")) for x in range(len(data) - 1)], 614 "3位率": [float(str(data[x + 1][9]).replace("%", "")) for x in range(len(data) - 1)], 615 "4位率": [float(str(data[x + 1][11]).replace("%", "")) for x in range(len(data) - 1)], 616 }, index=[data[x + 1][0] for x in range(len(data) - 1)] 617 ) 618 619 imgdata = graphing_rank_distribution(df, graph_title) 620 elements.append(Spacer(1, 5 * mm)) 621 elements.append(Image(imgdata, width=1200 * 0.5, height=700 * 0.5)) 622 623 elements.append(PageBreak()) 624 625 return elements
期間集計
Arguments:
- style (dict): レイアウトスタイル
Returns:
list: 生成内容
def
sectional_aggregate(style: dict, target_info: dict) -> list:
628def sectional_aggregate(style: dict, target_info: dict) -> list: 629 """区間集計 630 631 Args: 632 style (dict): レイアウトスタイル 633 target_info (dict): プレイヤー情報 634 635 Returns: 636 list: 生成内容 637 """ 638 639 elements: list = [] 640 641 pattern: list[tuple[int, int, str]] = [ 642 # 区切り回数, 閾値, タイトル 643 (80, 100, "短期"), 644 (200, 240, "中期"), 645 (400, 500, "長期"), 646 ] 647 648 for count, threshold, title in pattern: 649 if target_info["game_count"] > threshold: 650 # テーブル 651 elements.append(Paragraph(f"区間集計 ( {title} )", style["Left"])) 652 elements.append(Spacer(1, 5 * mm)) 653 data = get_count_results(count) 654 655 if not data: 656 return [] 657 658 tt = LongTable(data, repeatRows=1) 659 ts = TableStyle([ 660 ("FONT", (0, 0), (-1, -1), "ReportFont", 10), 661 ("GRID", (0, 0), (-1, -1), 0.25, colors.black), 662 ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), 663 ("ALIGN", (0, 0), (-1, -1), "CENTER"), 664 ('SPAN', (5, 0), (6, 0)), 665 ('SPAN', (7, 0), (8, 0)), 666 ('SPAN', (9, 0), (10, 0)), 667 ('SPAN', (11, 0), (12, 0)), 668 ('SPAN', (14, 0), (15, 0)), 669 # ヘッダ行 670 ("BACKGROUND", (0, 0), (-1, 0), colors.navy), 671 ("TEXTCOLOR", (0, 0), (-1, 0), colors.white), 672 ]) 673 if len(data) > 4: 674 for i in range(len(data) - 2): 675 if i % 2 == 0: 676 ts.add("BACKGROUND", (0, i + 2), (-1, i + 2), colors.lightgrey) 677 tt.setStyle(ts) 678 elements.append(tt) 679 680 # 順位分布 681 df = pd.DataFrame( 682 { 683 "1位率": [float(str(data[x + 1][6]).replace("%", "")) for x in range(len(data) - 1)], 684 "2位率": [float(str(data[x + 1][8]).replace("%", "")) for x in range(len(data) - 1)], 685 "3位率": [float(str(data[x + 1][10]).replace("%", "")) for x in range(len(data) - 1)], 686 "4位率": [float(str(data[x + 1][12]).replace("%", "")) for x in range(len(data) - 1)], 687 }, index=[f"{str(data[x + 1][0])} - {str(data[x + 1][1])}" for x in range(len(data) - 1)] 688 ) 689 690 imgdata = graphing_rank_distribution(df, f"順位分布 ( 区間 {title} )") 691 elements.append(Spacer(1, 5 * mm)) 692 elements.append(Image(imgdata, width=1200 * 0.5, height=800 * 0.5)) 693 694 # 通算ポイント推移 695 data = get_count_moving(count) 696 tmp_df = pd.DataFrame(data) 697 df = pd.DataFrame() 698 for i in sorted(tmp_df["interval"].unique().tolist()): 699 list_data = tmp_df[tmp_df.interval == i]["point_sum"].to_list() 700 game_count = tmp_df[tmp_df.interval == i]["total_count"].to_list() 701 df[f"{min(game_count)} - {max(game_count)}"] = [None] * (count - len(list_data)) + list_data 702 703 imgdata = graphing_total_points(df, f"通算ポイント推移(区間 {title})", True) 704 elements.append(Image(imgdata, width=1200 * 0.5, height=800 * 0.5)) 705 706 # 平均順位 707 df = pd.DataFrame() 708 for i in sorted(tmp_df["interval"].unique().tolist()): 709 list_data = tmp_df[tmp_df.interval == i]["rank_avg"].to_list() 710 game_count = tmp_df[tmp_df.interval == i]["total_count"].to_list() 711 df[f"{min(game_count)} - {max(game_count)}"] = [None] * (count - len(list_data)) + list_data 712 713 imgdata = graphing_mean_rank(df, f"平均順位推移(区間 {title})", True) 714 elements.append(Image(imgdata, width=1200 * 0.5, height=500 * 0.5)) 715 716 elements.append(PageBreak()) 717 718 return elements
区間集計
Arguments:
- style (dict): レイアウトスタイル
- target_info (dict): プレイヤー情報
Returns:
list: 生成内容