ゆっくり開発

開発したい時に開発するブログ

将棋ウォーズの直近の対局について,棋譜データを手軽に表示したい

将棋をはじめました

研究室の後輩に誘われて,最近はじめました. まだまだ初心者なので,定石を知ってるプレイヤーに狩られまくってます.

将棋の対局アプリとして,僕は日本将棋連盟公認の将棋アプリ「将棋ウォーズ」を使っています.

対局後の検討には主に「ぴよ将棋」というスマホアプリを使っています.

将棋ウォーズはプレイヤーのアカウント名でその人の対局結果(棋譜)を検索する機能があり, おそらく,ぴよ将棋はその機能を使って将棋ウォーズと連係しています. また,各対局結果について棋譜を検討する機能があります.

f:id:uttnaoki:20181123183735p:plain
f:id:uttnaoki:20181123183738p:plain

今は 将棋ウォーズで対局->ぴよ将棋で検討 の流れで勉強しています.

直近の対局データ(棋譜データ)をWebページで表示

ぴよ将棋を使っていれば必要ないのですが, 他のアプリやソフトを使うと,将棋ウォーズと直連携していない場合があります.

このため,対局結果を手軽に表示するWebページを作りました.

https://uttnaoki.github.io/kifu-viewer/?uid=uttnaoki

一番必要な処理だけを実装したので,デザインはこの上ないくらいポンコツです.

f:id:uttnaoki:20181123191825p:plain

各URLは以下のようなページにリンクしています.

f:id:uttnaoki:20181123191906p:plain

また,将棋ウォーズのユーザーIDをテキストフォームかクエリに入力とすると その人の対局結果に関するリンクが表示されます.

実装について

まず,ソースコードは以下のリポジトリです.

github.com

今回作ったページの実装は以下の流れになっています.

  1. 将棋ウォーズから対局結果一覧ページのHTMLを取得
  2. 取得したHTMLから各対局結果ページのURLを取得
  3. 将棋ウォーズの棋譜データを表示してくれる外部ページへのリンクを作成

将棋ウォーズからHTMLを取得するところで外部サイトにアクセスしているので, ブラウザがCORS関係のエラーを吐きます.

なので,間に別のサーバーを挟む実装にしています.

このため,以下のサイトの内1つでも動かなくなると今回開発したページは機能しなくなります.

  • GitHub
  • 中間サーバ
  • 将棋ウォーズ
  • 棋譜データ変換サイト

感想

やりたいことできたので満足. 気が向いたらデザイン面と機能面を改善したい.

マイクラの画像出力について,続き

前回作成したプログラムについて,微修正しました.

変更点は以下の通り.

  • コードの修正(リファクタリング
  • ブロックの出力先をキャラクターの上からz軸方向に変更
  • 縦長画像と横長画像に対応

実際に動かしてみたものが↓です.


画像をマイクラで描画するプログラムについて,続き

また,ソースコードは以下です.

github.com

README.md はまだ書いてないです.

Minecraft上でスクリプトを実行してピカチュウを描いてみた

概要

今回の開発は過去3回の開発の続きで,集大成になるものです.
とりあえず過去3回の開発で出来たことを簡単に書きます.

開発1

入力画像をリサイズし,リサイズ後の画像の各ピクセルのRGBを取得

f:id:uttnaoki:20180924000232p:plain

開発2

画像を特定の色のみで描き直す処理を作成

f:id:uttnaoki:20180924000253p:plain

開発3

イクラでコマンドセット(スクリプト)を実行する方法を調査し, そのマイクラスクリプトを出力するpythonスクリプトを作成
f:id:uttnaoki:20180923235716g:plain

今回の開発でやったこと

今回は上記の内容を踏まえ,入力画像のピカチュウをマイクラ上に羊毛ブロックで描画するためのpythonスクリプトを作成しました.

ソースコード

今回作成したソースコードは以下の2つのファイルです.

  • mcblock.py
    羊毛ブロックのIDとRGBを格納した変数を宣言するファイルです. このRGB値は以下のサイトを参考にしました.

Wool – Official Minecraft Wiki

wool_colorcode = {
    'white_wool': '#E9ECEC',
    'orange_wool': '#F07613',
    'magenta_wool': '#BD44B3',
    'light_blue_wool': '#3AAFD9',
    'yellow_wool': '#F8C627',
    'lime_wool': '#70B919',
    'pink_wool': '#ED8DAC',
    'gray_wool': '#3E4447',
    'light_gray_wool': '#8E8E86',
    'cyan_wool': '#158991',
    'purple_wool': '#792AAC',
    'blue_wool': '#35399D',
    'brown_wool': '#724728',
    'green_wool': '#546D1B',
    'red_wool': '#A12722',
    'black_wool': '#141519'
}
  • make_drawfunc.py
    • 入力: 元画像
    • 出力:
      1. 羊毛ブロックのRGBで表現した100×100サイズの画像ファイル
      2. 羊毛ブロックでマイクラ上にピカチュウを描画するためのスクリプト(.mcfunction)
from PIL import Image
import math
import numpy as np
import mcblock

funcdir_path = 'functions'

# カラーコード文字列をRGBのタプルに変換する.
def cc2rgb (colorcode):
    r = int(colorcode[1:3], 16)
    g = int(colorcode[3:5], 16)
    b = int(colorcode[5:7], 16)
    return (r,g,b)

def resize_img (filename, size_tuple):
    # 既存ファイルを readモードで読み込み
    img = Image.open(filename, 'r')

    # リサイズ。サイズは幅と高さをtupleで指定
    return img.resize(size_tuple)

def reduce_img_color (base_img, rgb_set, filename):
    # rgbの距離(norm)を計算するためにnumpyの配列に変換
    rgb_set = np.array(rgb_set)
    rgb_set_len = len(rgb_set)

    # usable_rgb の中から最も this_rgb に近いものを返す
    def get_nearest_rgb_from_usable (this_rgb, usable_rgb):
        this_rgb = np.array(this_rgb)
        norm_set = [np.linalg.norm(this_rgb - rgb_set[i]) for i in range(rgb_set_len)]
        min_index = np.argmin(norm_set)
        return tuple(usable_rgb[min_index]), min_index

    # 指定されたピクセル(座標)の色を usable_rgb の色に修正する
    def mod_one_pixel (x, y):
        r,g,b = base_img.getpixel((x, y))
        modified_rgb, index = get_nearest_rgb_from_usable([r,g,b], rgb_set)
        base_img.putpixel((x, y), modified_rgb)
        return index

    # 画像の幅と高さを取得
    width, height = base_img.size
    # 画像の各ピクセルの色を usable_rgb の色に修正し,修正後のRGBを取得
    used_rgb_indexes = [mod_one_pixel(x, y) for y in range(height) for x in range(width)]
    # 修正後の画像を保存
    base_img.save(filename)

    return used_rgb_indexes

def get_command (block_name, index, canvas_size, transform):
    x = index%canvas_size[0] + transform['x']
    y = -(index//canvas_size[1]) + transform['y']
    return 'setblock ~{0} ~{1} ~ {2}'.format(x, y, block_name)

def get_command_set (block_names, selected_block_indexes, canvas_size):
    transform = {
        'x': -(canvas_size[0]//2),
        'y': canvas_size[1]+3
    }
    return [get_command(block_names[selected_block_indexes[i]], i, canvas_size, transform) for i in range(len(selected_block_indexes))]

def main (source_img_name):
    # 羊毛ブロックの名前(ID)を抽出
    wool_names = [name for name in mcblock.wool_colorcode]
    # 羊毛ブロックのRGBを抽出
    usable_rgb_set = [cc2rgb(cc) for cc in mcblock.wool_colorcode.values()]

    # 出力画像のサイズ
    output_img_size = (100, 100)

    # 入力画像をリサイズ
    resized_img = resize_img(source_img_name, output_img_size)
    # RGBに変換
    rgb_img = resized_img.convert('RGB')

    # usable_rgb_set で表現した画像を保存し,使われたRGBとそのindexを取得.
    used_rgb_indexes = reduce_img_color(rgb_img, usable_rgb_set, 'wool_mode.png')

    command_set = get_command_set(wool_names, used_rgb_indexes, output_img_size)
    funcfile_path = '{0}/draw_pikachu.mcfunction'.format(funcdir_path)
    with open(funcfile_path, 'w', encoding='utf-8') as f:
        [f.write('{0}\n'.format(c)) for c in command_set]

if __name__ == '__main__':
    main('pikachu.png')

実行結果

実際にマイクラピカチュウを描いてみた.


マイクラで関数を実行してピカチュウを描いてみた

感想

前々々回の開発でこれができたら面白いなぁと思ってたことがちゃんとできてめっちゃうれしいです.

切りの良いところまでいったのでとりあえずブログに描いたけど, 細かいところを色々できてないので,以下に関して後でやっとこうと思います.

  • コードのリファクタリング
    今回のコードちょっと適当に描いてるし,今GitHubに上げてるリポジトリ名も適当なので,コードをリファクタリングして,ちゃんとした名前でGitHubにpushしたいです.
  • 羊毛ブロック以外のブロックにも対応
    例えばガラスブロックとか,粘土ブロックとか,他にも使えそうなブロックがあるので,それらにも対応したいです.

あと,以下は気分次第で取り組もうかなと思ってます.

  • Qiitaデビュー
    今回の開発はマイクラの仕様の調査から画像処理といったボリュームのある開発だったので,内容をまとめてQiitaに記事を描きたい.
    まだQiitaに記事を書いたことがないので,これを機にデビューするのはあり.
  • 動画にまとめる
    動画編集とかやってみたいので,今回やってたマイクラスクリプトを実行する(前回の開発)という部分について, わかりやすい動画を作ってみたい.

マイクラ用のスクリプトを出力するスクリプトを作ってみた

概要

  • MInecraft(以降,マイクラ) では,チートコマンドを使うことで好きな場所に好きなブロックを設置できる.
  • このシステムを使い,羊毛などのカラフルなブロックを並べ, マイクラ上で絵を描きたい.
  • ここで,チートコマンドについて,マイクラの ver 1.12 から実装されたfunctionシステムを使うことで,スクリプト化できる.
  • 今回は「羊毛を並べるスクリプト(.mcfunction)」を「出力するスクリプト(.py)」を作成する.

また,今回対象とするマイクラのバージョンは1.13.1である.
1.121.13ではコマンドやfunctionの仕様が少し違うので, 今回の内容は1.12に対応しない.

コマンドで羊毛ブロックを設置する

イクラでは,以下のコマンドで好きな場所に好きなブロックを設置できる.

/setblock <x> <y> <z> <ブロックID>

この形式はver 1.12でも有効である.しかし,1.121.13ではブロックIDが異なる.
わかりにくいかもしれないが,以下のサイトに両バージョンのブロックIDが載っている.

minecraftitemids.com

上に示したコマンドの<x> <y> <z>でブロックを設置したい座標を指定する.座標を指定する方法には絶対座標相対座標の2種類がある.

  • 絶対座標
    絶対座標(100, 100, 100)に白の羊毛ブロックを設置するコマンドは以下の様になる.
    /setblock 100 100 100 minecraft:white_wool
  • 相対座標
    相対座標を指定する時は,各xyzの値に~を付ける. 例えば,プレイヤーの2ブロック上に白の羊毛ブロックを設置するコマンドは以下の様になる.
    /setblock ~ ~2 ~ minecraft:white_wool

イクラスクリプトを実行する

スクリプトについて

イクラでは,以下の様にファイルに記述されたスクリプトを実行するシステムがある.

setblock ~-1 ~6 ~ minecraft:white_wool
setblock ~0 ~6 ~ minecraft:orange_wool
setblock ~1 ~6 ~ minecraft:magenta_wool

スクリプトを実行するためには以下4点に注意する必要がある.

  • 各コマンドの先頭に\(スラッシュ)は記述してはいけない
  • ファイル名はアルファベット(≠マルチバイト)とし,拡張子は.mcfunctionとする
  • ファイルはBOMなしUTF-8 (UTF-8N) で記述する
  • イクラのワールドの初期状態はスクリプトを実行できる設定になっておらず,一手間かけなければならない.

スクリプトを実行できるようにする

以下のサイトを参考にした.1.12のバージョン用のことも書いてあってとてもいい.

hollys-command-lecture.hatenablog.com

スクリプトを実行できるようにするために, 以下のディレクトリについて,

minecraft/saves/(ワールド名)/datapacks

以下の様にファイルとディレクトリを追加する.

  • utt/pack.mcmeta
    このファイルには以下の内容をUTF-8Nで記述する.
{
"pack": {
"pack_format": 1,
"description": "datapack"
}
}
  • utt/data/uttscripts/functions/
    このディレクトリにfunc.mcfunctionなどのファイルを配置する.

上記パスの青字で示したディレクトリは任意の文字列でいい.ただし,マルチバイトは不可.

また,青字で示したディレクトリの内,uttscriptsの方はスクリプトを呼び出す際に入力する文字列になるので, わかりやすい文字列の方がいい.

以下はfunc.mcfunctionスクリプトを呼び出すコマンドの例.

/function uttscripts:func

スクリプト(.mcfunction)を出力するスクリプト(.py)を作成

描画したいエリアの縦と横のサイズを変数で指定し, そのサイズのエリアを羊毛で埋めるスクリプトを作成した.

細かい説明はめんどうなのでとりあえずコードを載せます.

micrablock_name.py

各羊毛ブロックのIDを格納した配列を定義

wool = [
    'minecraft:white_wool',
    'minecraft:orange_wool',
    'minecraft:magenta_wool',
    'minecraft:light_blue_wool',
    'minecraft:yellow_wool',
    'minecraft:lime_wool',
    'minecraft:pink_wool',
    'minecraft:gray_wool',
    'minecraft:light_gray_wool',
    'minecraft:cyan_wool',
    'minecraft:purple_wool',
    'minecraft:blue_wool',
    'minecraft:brown_wool',
    'minecraft:green_wool',
    'minecraft:red_wool',
    'minecraft:black_wool'
]

make_micrafunc_test.py

import os
import micrablock_name as block_name

funcdir_path = 'functions'

funcfile_path = '{0}/func50.mcfunction'.format(funcdir_path)

canvas_width = 50
canvas_height = 50
block_num = len(block_name.wool)
transform = {
    'x': -(canvas_width//2),
    'y': canvas_height+3
}

def get_block_name (x, y):
    return block_name.wool[(canvas_width*y + x)%block_num]

def get_command (x, y):
    tx = x + transform['x']
    ty = -y + transform['y']
    return 'setblock ~{0} ~{1} ~ {2}'.format(tx, ty, get_block_name(x, y))

commands = [get_command(x, y) for y in range(canvas_height) for x in range(canvas_width)]


with open(funcfile_path, 'w', encoding='utf-8') as f:
    [f.write('{0}\n'.format(c)) for c in commands]

出力したスクリプト(.mcfunction)を実行

↓の5種類の大きさで羊毛を敷き詰める処理を実行してみた。

  • 3×3
  • 10×10
  • 20×20
  • 50×50
  • 100×100


マイクラで関数を実行して処理能力を調べてみた

上記処理を実行したのはMacbookProでGPUの拡張もしていないのでゲームに適したパソコンではない. 普通にマイクラをプレイするだけでもめっちゃ熱がこもる.

しかし,100×100の大きさの描画にもゲームが重くなることなく 1瞬で処理が終わったので, ブロックを敷き詰める処理はそこまで重い処理ではないことがわかる.

感想

今回は元々以下を調査したいという狙いがあった.

  1. イクラでコマンドをスクリプト化できるのか
  2. 大きな範囲でブロックを敷き詰める処理は実行できるのか (できないなら,どの範囲までならできそうか)

イクラの最新バージョン(1.13.1)でスクリプトの実行が可能であることがわかり, また,100×100のサイズでブロックを敷き詰める(10000回のコマンド実行)処理が実行できることがわかり, 今回の目的を無事達成できた.

次は,前回と前々回で作ったプログラムを利用し,任意の画像をマイクラ上で描画できるようにしたい.

ドット絵を少ない配色で描き直す

概要

前回の記事 の続きです.
前回作成したプログラムは以下の処理を実行できます.

  1. 入力画像のサイズを小さくすることで容量の小さいドット絵を取得
  2. ドット絵を解析し,使われているRGBの種類とそれが何ピクセル使われているかを取得
  3. 使われているRGBについて,使用頻度の多い順に並び替えた画像を作成

今回は2.ドット絵の解析で取得したデータから, 使用頻度の多いRGBをn個取得し,
そのRGBだけを使用して元のドット絵を描き直すプログラムを作成しました.

色の補完方法について

今回のプログラムでは,使用するRGBを決めた後,そのRGBが使われていないピクセルのRGBを書き換える処理が必要になり, また,どのRGBに書き換えるかを選択するアルゴリズムが必要になります.

今回は,RGBを3次元のベクトルと捉え,使用可能RGBとのノルムを計算することで,最適なRGBを選択しました.

以下はその計算の例

f:id:uttnaoki:20180902191859p:plain:h300

上記例では,補完対象のRGBは使用可能RGBの2番目のものに最も近い. このため,補完対象のrgb(204,96,90)rgb(204,105,108)に補完することとします.

この操作を全ピクセルに対して行います.

作成した画像

元画像から使用頻度の多いRGBを取得し, 上位{10, 20, 30, 40}個を使用可能RGBとし,
それらのRGBだけを使用してドット絵を描き直しました.

以下は元のドット絵(230色)

f:id:uttnaoki:20180901144422p:plain:w300

以下は使用するRGBの数を減らした画像

  • 10色
    f:id:uttnaoki:20180902095047p:plain:w300
  • 20色
    f:id:uttnaoki:20180902095057p:plain:w300
  • 30色
    f:id:uttnaoki:20180902095105p:plain:w300
  • 40色
    f:id:uttnaoki:20180902095113p:plain:w300

使用可能RGBが30種類を超えるといい感じの色合いになる.
30と40の差はあまりないので,ピカチュウに関しては多くても30種類のRGBでいい感じに描けることがわかった.

ソースコード

前回の記事 に以下のコードを追加した.

module

numpy を使ったので,以下のコードを追加した.

import numpy as np

関数

指定のRGBセットで画像を再描画する関数を追加した.

def make_less_color_img (base_img, rgb_set, filename):
    # rgbの距離(norm)を計算するためにnumpyの配列に変換
    rgb_set = np.array(rgb_set)
    rgb_set_len = len(rgb_set)

    # usable_rgb の中から最も this_rgb に近いものを返す
    def get_nearest_rgb_from_usable (this_rgb, usable_rgb):
        this_rgb = np.array(this_rgb)
        norm_set = [np.linalg.norm(this_rgb - rgb_set[i]) for i in range(rgb_set_len)]
        min_index = np.argmin(norm_set)
        return tuple(usable_rgb[min_index])

    # 指定されたピクセル(座標)の色を usable_rgb の色に修正する
    def mod_one_pixel (x, y):
        r,g,b = base_img.getpixel((x, y))
        base_img.putpixel((x, y), get_nearest_rgb_from_usable([r,g,b], rgb_set))

    # 画像の幅と高さを取得
    width, height = base_img.size
    # 画像の各ピクセルの色を usable_rgb の色に修正する
    [mod_one_pixel(x, y) for x in range(width) for y in range(height)]
    # 修正後の画像を保存
    base_img.save(filename)

main関数

上記関数を呼ぶために以下のコードをmain関数に追加した.

    def get_frequent_color (rgb_count):
        sorted_rgb_count = sorted(rgb_count.items(), key=lambda x: -x[1])
        return [list(map(int, rgb[0].split('-'))) for rgb in sorted_rgb_count]

    # リサイズ後の画像に使われている色の数を出力
    print('color_num: {0}'.format(len(img_pixel_colors)))

    # less_color.png で用いる色の数を定義
    usable_rgb_num = 30
    # 使用面積(頻度)の大きい色から usable_rgb_num の数分取ってくる
    usable_rgb_set = get_frequent_color(img_pixel_colors)[:usable_rgb_num]
    # usable_rgb_set の色だけで元画像(モザイク)を表現し,保存
    make_less_color_img(rgb_img, usable_rgb_set, 'less_color.png')

感想

  • 30色はちょっと多すぎるので,もっと少ない色でいい感じに表現したい.
  • 今回は元画像に使われているRGBをそのまま使った処理を行ったので, 次は事前に色を手動で選択して,それらの色を使っていい感じに描画したい.

画像に使われている色(RGB)を取得する

作ったもの

タイトルの通り,入力画像からその画像に使われているRGBを取得するプログラムを作成.ただし,入力画像は 100×100 にリサイズして扱う.

以下は入力画像とリサイズ後の画像

f:id:uttnaoki:20180901144040p:plain:w300 f:id:uttnaoki:20180901144422p:plain:w300

また,取得したRGBについて,各RGBが何ピクセル使われているかをカウントし,それらを降順で出力.

以下は出力結果の例

f:id:uttnaoki:20180901144217p:plain:w300

その後,使用頻度の多いRGBから画像に並べ,color_map.pngとして出力.

こんな感じ

f:id:uttnaoki:20180901144352p:plain:w300

ソースコード

from PIL import Image
import math

def resize_img (filename, size_tuple):
    # 既存ファイルを readモードで読み込み
    img = Image.open(filename, 'r')

    # リサイズ。サイズは幅と高さをtupleで指定
    return img.resize(size_tuple)

def count_pixel_colors (img):
    # 集計結果を格納する辞書配列
    img_pixel_colors = {}

    # 指定された座標のピクセルの RGB を取得し,そのRGBのカウンターをインクリメント
    def increment_color_counter (x, y):
        # (x,y)座標のピクセルの rgb を取得
        r,g,b = img.getpixel((x,y))
        # rgbを辞書配列のキーに使うために文字列化 (ex. 238-211-114)
        rgb = '-'.join([str(n) for n in [r,g,b]])
        # 取得した rgb が key になければ 値0 で追加
        img_pixel_colors.setdefault(rgb, 0)
        # 取得した rgb のカウンターをインクリメント
        img_pixel_colors[rgb] += 1

    # 画像の幅と高さを取得
    width, height = img.size
    # 画像に使われている色の数を集計し,辞書配列で img_pixel_colors に格納
    [increment_color_counter(x, y) for x in range(width) for y in range(height)]

    return img_pixel_colors

# ピクセル数の多い色順にピクセルを並べた画像を作成
def make_color_map (img_size, rgb_set, filename):
    img = Image.new('RGB', img_size)
    x=0
    y=0
    for rgb, count in sorted(rgb_set.items(),  key=lambda x: -x[1]):
        while count > 0:
            rgb_tuple = tuple([int(v) for v in rgb.split('-')])
            img.putpixel((x, y), rgb_tuple)
            x+=1
            if x == img_size[0]:
                x=0
                y+=1
            count-=1
    img.save(filename)

def main (img_name):
    # 出力画像のサイズ
    outimg_size = (100, 100)

    # 画像をリサイズすることでモザイク画像を取得
    mosaic_img = resize_img('pikachu.png', outimg_size)
    # 取得したモザイク画像を保存
    mosaic_img.save('mosaic_img.png', 'PNG', quality=100, optimize=True)

    # RGBに変換
    rgb_img = mosaic_img.convert('RGB')

    # 使われている色の種類を取得し,それらのピクセル数をカウント
    img_pixel_colors = count_pixel_colors(rgb_img)

    for k, v in sorted(img_pixel_colors.items(),  key=lambda x: -x[1]):
        print(k, v)

    # ピクセル数の多い色順にピクセルを並べた画像を作成
    make_color_map(outimg_size, img_pixel_colors, 'color_map.png')

if __name__ == '__main__':
    main('pikachu.png')

今後の展開

リサイズ処理で生成する画像について,使用するRGBを制限したい. 指定のRGBで画像を表現できるようにしたい. 例えば,マイクラのブロックの色だけで表現するとか.

追記:Vue + D3 + Leaflet でヒートマップを描画する時に詰まったところ

概要

前回の記事でタイトルの内容についてメモしたが, その後別の問題が発生したので,それについて簡単にメモする.

問題

Leaflet はバージョン0.7.7から1.0.0の間のどこかで mouse event 関係のバグが発生してるっぽい.

バグといっても,普通に Leaflet のマップをマウスで操作する分には問題ない. しかし,前回の記事のように Leaflet 上に d3 で svg を描画する際, その svg に対して mouse event が無効になってしまう.

以下の画像を見ると,svg タグ のスタイルが
pointer-events = "none"
になっているのがわかる.

f:id:uttnaoki:20180822085350p:plain

f:id:uttnaoki:20180822085407p:plain

解決方法

以下のページを参考にした.

github.com

d3でpathを描画する処理に以下のコードを書けばいいっぽい
.attr('style', 'pointer-events:visiblePainted;')
また,以下のコードでも同じことである.
.style('pointer-events', 'visiblePainted')

前後のコードと一緒に書くとこんな感じ

var featureElement = svg.selectAll("path")
    .data(data.features)
    .enter()
    .append("path")
    .style('pointer-events', 'visiblePainted') // ここ!
    .attr("stroke", "gray")
    .attr("fill", "green")
    .attr("fill-opacity", 0.6)
    .on('mouseover', (d) => {console.log(d);})