■ はじめに
仕事でやりたいことが全く手が付けられないので 超個人的なツールの元なるソースをメモ。
https://dk521123.hatenablog.com/entry/2023/06/18/102448
https://dk521123.hatenablog.com/entry/2023/06/14/174104
を組み合わせて、1つのDOTファイルから、 Nodeの追加・削除により、 d3-graphviz を使ってアニメーション表示ツールを作る
目次
【1】ツール仕様 Step1: アニメーションの元になるDOTファイルを作成する Step2: HTML+JSによりアニメーション表示 【2】フォルダ構成例 1)delete 2)add 3)input 【3】サンプル
【1】ツール仕様
* 以下。
Step1: アニメーションの元になるDOTファイルを作成する
[1] 元となるDOTファイルに対して、要素を削除・追加 [2] 削除・追加には、別のTextファイルから読み込み [1]のファイルの対して。 => オプションとして、削除前に色を変更して 削除対象を分かりやすくする [3] そのファイルをDOTファイル群として生成する
Step2: HTML+JSによりアニメーション表示
[1] 1)のDOTファイルを随時読み込み、 JavaScriptの配列を追加したHTMLファイルを生成する
【2】フォルダ構成例
+ dot_animation_creator.py + delete + 01.txt + 02.txt + 03.txt + add + 01.txt + 03.txt + input + source.dot
1)delete
01.txt
table3 table4
02.txt
table9
03.txt
table10
2)add
01.txt
"table1" -> "table20" -> "table21";
02.txt
"table1" -> "table22" -> "table23";
3)input
source.dot
digraph { ranksep=0.3; nodesep=0.7; rankdir="TB" {rank = source; "table1";} "table1"-> "table2"; "table2" -> "table3"; "table4" -> "table2"; "table2" -> "table5"; "table2" -> "table6"; "table6" -> "table7"; "table6" -> "table8"; "table8" -> "table9"; "table7" -> "table10"; "table8" -> "table4"; # ADD HERE, IF YOU WANT }
【3】サンプル
import os import re import shutil class DotAnimationCreator: REPLACED_SIGN = ' # ADD HERE, IF YOU WANT' def __init__( self, input_file_path='./input/source.dot', delete_path='./delete', add_path='./add', output_path='./out1', output_file_name_format='out_{:0=2}-{:0=1}.dot' ) -> None: self.input_file_path = input_file_path self.delete_path = delete_path self.add_path = add_path self.output_path = output_path self.output_file_name_format = output_file_name_format def _get_lines_from_file(self, file_full_path, encoding='utf-8'): with open(file_full_path, 'r', encoding=encoding) as file: return file.read().splitlines() def _save(self, file_full_path, content, encoding='utf-8'): with open(file_full_path, 'w', encoding=encoding) as file: file.write(content) def _create_dot_to_change_color(self, delete_list, no, input_list): dot = '\n'.join(input_list) for delete_data in delete_list: print(f'[{no}] - {delete_data}') if (re.search(f'"{delete_data}" \[.*\];', dot)): dot = re.sub( f'"{delete_data}" \[.*\];', f'"{delete_data}" [shape="box" color="red"];', dot ) else: dot = dot.replace( self.REPLACED_SIGN, f'{self.REPLACED_SIGN}\n\t"{delete_data}" [shape="box" color="red"];' ) output_full_path = self._get_output_full_path(no, True) self._save(output_full_path, dot) def _get_output_full_path(self, no, is_change_color=False): index = 0 if is_change_color else 1 output_file_name = self.output_file_name_format.format(no, index) return f'{self.output_path}/{output_file_name}' def _marge_add_list(self, input_list, add_list): replaced_sign_index = input_list.index(self.REPLACED_SIGN) insert_index = replaced_sign_index + 1 input_list[insert_index:insert_index] = add_list def _generate_dots(self, can_change_color=True): input_list = self._get_lines_from_file(self.input_file_path) for i, file_name in enumerate(os.listdir(self.delete_path)): no = i + 1 print(f'[{no}] - file_name') # Delete part delete_file_full_path = os.path.join(self.delete_path, file_name) delete_list = self._get_lines_from_file(delete_file_full_path) if not delete_list or len(delete_list) == 0: print('No target to delete') continue # Before delete, change the color if can_change_color: self._create_dot_to_change_color(delete_list, no, input_list) # Exclusion for deleting target data for delete_data in delete_list: input_list = [line for line in input_list if f'"{delete_data}"' not in line] # Add part add_file_full_path = f'{self.add_path}/{file_name}' if not os.path.exists(add_file_full_path): print(f'[{no}] No exist {add_file_full_path}') else: # Exist add file add_list = self._get_lines_from_file(add_file_full_path) if not add_list or len(add_list) == 0: print(f'[{no}] No target to add') else: # Merge self._marge_add_list(input_list, add_list) output_full_path = self._get_output_full_path(no, False) dot = '\n'.join(input_list) self._save(output_full_path, dot) return self.output_path def _generate_html_with_animation(self, html_file_path, output_path): dot_contents = self._read_dot_contents(output_path) html_content = self._generate_html_content(dot_contents) self._save(html_file_path, html_content) return html_file_path def _read_dot_contents(self, root_output_dot_path): file_list = os.listdir(root_output_dot_path) file_list.sort() file_contents = [] for dot_file_name in file_list: do_file_path = os.path.join(root_output_dot_path, dot_file_name) with open(do_file_path, 'r', encoding='UTF-8') as file: file_content = file.read() file_contents.append(file_content) return file_contents def _generate_html_content(self, dot_contents): content = ''' <!DOCTYPE html> <html> <meta charset="utf-8"> <body> <script src="https://d3js.org/d3.v5.min.js"></script> <script src="https://unpkg.com/viz.js@1.8.1/viz.js" type="javascript/worker"></script> <script src="https://unpkg.com/d3-graphviz@2.1.0/build/d3-graphviz.min.js"></script> <style> .styled { border: 0; line-height: 2.0; padding: 0 20px; font-size: 1rem; text-align: center; color: #fff; text-shadow: 1px 1px 1px #000; border-radius: 10px; background-color: rgba(255,0,0,0.3); background-image: linear-gradient(to top left, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.2) 30%, rgba(0, 0, 0, 0)); box-shadow: inset 2px 2px 3px rgba(255, 255, 255, 0.6), inset -2px -2px 3px rgba(0, 0, 0, 0.6); } </style> <button id="next" class="favorite styled" onclick="showDot(1)"> Next </button> <button id="previous" class="favorite styled" onclick="showDot(-1)"> Previous </button> <form> <textarea id="textArea" name="txt" rows="10" cols="100"></textarea> </form> <div id="graph" style="text-align: center;"></div> </div> <script> var dots = [ ''' for dot in dot_contents: content = content + '`' content = content + dot + '`,\n' content = content + ''' ]; var dotIndex = -1; var graphviz = d3.select("#graph").graphviz() .transition(function () { return d3.transition("main") .ease(d3.easeBounceOut) .delay(500) .duration(1000); }) .logEvents(true); function showDot(val) { dotIndex = (dotIndex + (val)) % dots.length; if (dotIndex < 0) { dotIndex = dots.length -1; } var dot = dots[dotIndex]; document.getElementById("textArea").value = dot; graphviz.renderDot(dot); } </script> </body> </html> ''' return content def execute( self, html_file_path='./dot_animation_viewer.html', can_change_color=True ): # Step0: Copy source file to output source_output_path = self._get_output_full_path(0) shutil.copyfile(self.input_file_path, source_output_path) # Step1: To generate DOTs output_path = self._generate_dots(can_change_color) # Step2: To generate HTML return self._generate_html_with_animation(html_file_path, output_path) if __name__ == '__main__': dot_creator = DotAnimationCreator() output = dot_creator.execute() print(f'DONE... See {output}')
関連記事
DOT言語表示ツール ~ d3-graphviz ~
https://dk521123.hatenablog.com/entry/2023/06/18/102448
Python + DOT言語で図作成するには
https://dk521123.hatenablog.com/entry/2023/06/14/174104
DOT言語 ~ 基礎知識編 ~
https://dk521123.hatenablog.com/entry/2023/06/15/004815
DOT言語 ~ 入門編 ~
https://dk521123.hatenablog.com/entry/2023/06/16/000531
Python ~ 基本編 / ファイル読込・書込 ~
https://dk521123.hatenablog.com/entry/2019/10/07/000000
Python ~ 基本編 / パス情報抽出 ~
https://dk521123.hatenablog.com/entry/2022/02/23/000000
Python ~ 基本編 / 正規表現 ~
https://dk521123.hatenablog.com/entry/2019/09/01/000000
Python ~ 基本編 / ラムダ lambda ~
https://dk521123.hatenablog.com/entry/2019/09/23/000000