ヒートマップ可視化ツール『影に潜む銘文にて鍵盤は赤熱す』 ソースコード

以下は『影に潜む銘文にて鍵盤は赤熱す』のソースコード全文です:


        import tkinter as tk
        from tkinter import ttk
        from collections import Counter
        import colorsys
        
        # キーボード配列
        KEYBOARD_LAYOUTS = {
            'ja/en': [
                ['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'],
                ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l'],
                ['z', 'x', 'c', 'v', 'b', 'n', 'm'],
            ],
            'ita': [
                ['', '', '', '', '', '', '', '', '', '', '', 'ì'],
                ['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', 'è'],
                ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'ò', 'à', 'ù'],
                ['z', 'x', 'c', 'v', 'b', 'n', 'm'],
            ],
            'esp': [
                ['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'],
                ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'ñ', '', 'ç'],
                ['z', 'x', 'c', 'v', 'b', 'n', 'm'],
            ],
            'fra': [
                ['', 'é', '', '', '', '', 'è', '', 'ç', 'à', ''],
                ['a', 'z', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'],
                ['q', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'ù'],
                ['w', 'x', 'c', 'v', 'b', 'n']
            ],
            'deu': [
                ['', '', '', '', '', '', '', '', '', '', 'ß'],
                ['q', 'w', 'e', 'r', 't', 'z', 'u', 'i', 'o', 'p', 'ü'],
                ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'ö', 'ä'],
                ['y', 'x', 'c', 'v', 'b', 'n', 'm']
            ],
            'rus': [
                ['ё'],
                ['й', 'ц', 'у', 'к', 'е', 'н', 'г', 'ш', 'щ', 'з', 'х', 'ъ'],
                ['ф', 'ы', 'в', 'а', 'п', 'р', 'о', 'л', 'д', 'ж', 'э'],
                ['я', 'ч', 'с', 'м', 'и', 'т', 'ь', 'б', 'ю']
            ]
        }
        
        VALID_CHARS = set("abcdefghijklmnopqrstuvwxyzìèòàùñçéèçàùßäüöабвгдеёжзийклмнопрстуфхцчшщъыьэюя")
        
        def frequency_to_color(freq, max_freq):
            ratio = freq / max_freq if max_freq else 0
            hue = (1 - ratio) * 0.75
            r, g, b = colorsys.hsv_to_rgb(hue, 1.0, 1.0)
            return f'#{int(r*255):02x}{int(g*255):02x}{int(b*255):02x}'
        
        def count_chars(text):
            return Counter(c.lower() for c in text if c.lower() in VALID_CHARS)
        
        class HeatmapKeyboardApp:
            def __init__(self, root):
                self.root = root
                self.root.title("キーボード・ヒートマップ - 影に潜む銘文にて鍵盤は赤熱す")
        
                self.text_area = tk.Text(root, height=10, font=("Arial", 12))
                self.text_area.pack(fill=tk.BOTH, padx=10, pady=(10, 0))
        
                self.lang_var = tk.StringVar(value='ja/en')
                lang_menu = ttk.OptionMenu(root, self.lang_var, 'ja/en', *KEYBOARD_LAYOUTS.keys(), command=self.update_keyboard)
                lang_menu.pack(pady=5)
        
                self.bottom_frame = tk.Frame(root)
                self.bottom_frame.pack(pady=10, padx=10, fill=tk.X)
        
                self.keyboard_frame = tk.Frame(self.bottom_frame)
                self.keyboard_frame.pack(side=tk.LEFT)
        
                self.legend_canvas = tk.Canvas(self.bottom_frame, width=30, height=120)
                self.legend_canvas.pack(side=tk.LEFT, padx=10)
        
                update_btn = ttk.Button(root, text="ヒートマップ更新", command=self.update_keyboard)
                update_btn.pack(pady=(0, 10))
        
                romaji_btn = ttk.Button(root, text="日本語をローマ字へ", command=self.open_romaji_converter)
                romaji_btn.pack(pady=(0, 10))
        
                self.update_keyboard()
        
            def update_keyboard(self, *args):
                for widget in self.keyboard_frame.winfo_children():
                    widget.destroy()
                self.legend_canvas.delete("all")
        
                text = self.text_area.get("1.0", tk.END)
                freq = count_chars(text)
                max_freq = max(freq.values(), default=1)
                layout = KEYBOARD_LAYOUTS[self.lang_var.get()]
        
                for row_idx, row in enumerate(layout):
                    for col_idx, key in enumerate(row):
                        key_disp = "Space" if key == ' ' else key.upper()
                        f = freq.get(key.lower(), 0)
                        if f == 0:
                            color = '#000000'
                            fg_color = '#FFFFFF'
                        else:
                            color = frequency_to_color(f, max_freq)
                            fg_color = '#FFFFFF'
        
                        label = tk.Label(self.keyboard_frame, text=key_disp,
                                         bg=color, fg=fg_color, width=5, height=2,
                                         font=("Helvetica", 12, "bold"), relief='ridge', borderwidth=2)
                        label.grid(row=row_idx, column=col_idx, padx=3, pady=3)
        
                self.draw_legend()
        
            def draw_legend(self):
                steps = 10
                for i in range(steps):
                    freq = (steps - i)
                    color = frequency_to_color(freq, steps)
                    y0 = i * 12
                    y1 = y0 + 12
                    self.legend_canvas.create_rectangle(0, y0, 30, y1, fill=color, outline="")
        
                self.legend_canvas.create_text(15, 0, anchor='n', text="多", fill="white", font=("Arial", 9, "bold"))
                self.legend_canvas.create_text(15, 120, anchor='s', text="少", fill="white", font=("Arial", 9, "bold"))
        
            def open_romaji_converter(self):
                def convert_to_romaji():
                    from pykakasi import kakasi
                
                    kks = kakasi()
                    input_text = input_box.get("1.0", tk.END)
                    result = kks.convert(input_text)
                    output_text = "".join([item['hepburn'] for item in result])
                
                    output_box.delete("1.0", tk.END)
                    output_box.insert(tk.END, output_text)
        
                win = tk.Toplevel(self.root)
                win.title("日本語 → ローマ字")
        
                tk.Label(win, text="日本語入力").pack(pady=(10, 0))
                input_box = tk.Text(win, height=5, font=("Arial", 12))
                input_box.pack(fill=tk.BOTH, padx=10, pady=5)
        
                convert_btn = ttk.Button(win, text="変換", command=convert_to_romaji)
                convert_btn.pack(pady=5)
        
                tk.Label(win, text="ローマ字出力").pack()
                output_box = tk.Text(win, height=5, font=("Arial", 12))
                output_box.pack(fill=tk.BOTH, padx=10, pady=(5, 10))
        
        if __name__ == "__main__":
            root = tk.Tk()
            app = HeatmapKeyboardApp(root)
            root.mainloop()