Claude CodeにVega-Liteのグラフを書かせる — vl-convertでPNG化して確かめる
Vega-Liteの仕様JSONをClaude Codeに書かせ、vl-convertでPNGにして目視で確かめる手順です。スキーマ検証を通っても空のグラフになる誤りと、その検出スクリプトも載せます。
グラフをPythonやJavaScriptのコードで書かせると、確認のたびに実行環境の差が混ざります。Vega-Liteなら、グラフ全体が1つのJSONです。データ、マーク(棒や線)、軸への割り当てを宣言するだけなので、Claude Codeが書くものは仕様JSON1本に絞れます。
そのJSONをvl-convertでPNGにして、Claude Codeに画像を見せれば「書く → 描く → 見る → 直す」のループが閉じます。この記事は、そのループを手元で動かす手順です。途中で、検証を通るのに空のグラフが出る誤りも見つかりました。その検出方法まで書きます。
Vega-Liteの仕様JSONはClaude Codeに何を任せやすいか
Vega-Liteは、インタラクティブなグラフを簡潔なJSON構文で記述する高水準の文法です。コンパイラが、Vega-Liteの仕様をより低水準なVegaの仕様に変換します。
最小の仕様に必要なのは、データ、mark、encodingの3つです。軸・凡例・スケールは、指定しなければ自動で決まります。これは任せる側にとって都合がよい性質です。
- Claude Codeが書くのは、JSON 1ファイル
- 差分がJSONの行単位で見える
- 実行時の依存は、描画側のvl-convertだけ
一方で、JSONのキー名やフィールド名の間違いは、書いた本人には見つけにくいものです。そこで描画と検証を、Claude Code自身に回させます。
vl-convertを用意する
vl-convertは、Vega-Liteの仕様をSVG・PNG・PDFなどに変換するツールです。VlConvertはVegaとVega-Liteの公式JavaScriptライブラリを内蔵しており、ブラウザもNode.jsも要りません。提供形態はRustライブラリ、CLI、Pythonパッケージの3つです。
この記事ではPythonパッケージを使います。仮想環境に入れておけば、プロジェクトを汚しません。
CLIから使うなら、cargo install vl-convert --lockedで入ります。READMEの例はvl-convert vl2svg --input chart.vl.json --output chart.svgの形で、入力と出力を省くと標準入力と標準出力になります。PNGまでをスクリプトで一括処理したいので、ここではPython側を選びます。
python3 -m venv .venv
.venv/bin/pip install vl-convert-python jsonschema
mkdir -p charts tools schema
curl -sLo schema/vega-lite-v6.json \
https://vega.github.io/schema/vega-lite/v6.json手元ではvl-convert-pythonの1.9.0が入りました。READMEの導入例は2.0系のプレリリースを指していますが、ここで使うvegalite_to_pngの呼び方は同じです。入るバージョンは時期で変わるので、pip listで確かめてください。
1.9.0が受け付けるVega-Liteのバージョンは、5.8から6.4までです。仕様の先頭に書く$schemaは、https://vega.github.io/schema/vega-lite/v6.jsonで足ります。この宣言があると、対応エディターでの検証と補完も効きます。
最初の1枚を描く
データはdata.valuesにそのまま埋め込みます。月別の新規登録数を、プラン別の折れ線にします。
{
"$schema": "https://vega.github.io/schema/vega-lite/v6.json",
"title": "Monthly signups by plan",
"width": 360, "height": 220,
"data": {"values": [
{"month": "2026-07-01", "plan": "Free", "signups": 120},
{"month": "2026-08-01", "plan": "Free", "signups": 150},
{"month": "2026-09-01", "plan": "Free", "signups": 170},
{"month": "2026-07-01", "plan": "Pro", "signups": 40},
{"month": "2026-08-01", "plan": "Pro", "signups": 55},
{"month": "2026-09-01", "plan": "Pro", "signups": 80}
]},
"mark": {"type": "line", "point": true},
"encoding": {
"x": {"field": "month", "type": "temporal",
"timeUnit": "yearmonth", "title": "Month"},
"y": {"field": "signups", "type": "quantitative",
"title": "Signups"},
"color": {"field": "plan", "type": "nominal"}
}
}encodingの各チャンネルにfieldとtypeを組で書くのが基本形です。READMEの例でも、カテゴリはnominal、数値はquantitativeと指定しています。日付はtemporalにして、timeUnitで月単位に丸めました。
描画はPythonの2行です。
import json, vl_convert as vlc
spec = json.load(open("charts/signups.vl.json"))
open("charts/signups.png", "wb").write(
vlc.vegalite_to_png(spec, scale=2))scale=2で解像度を2倍にします。Claude Codeに画像を読ませるときは、文字が潰れない大きさにしておくと誤読が減ります。
検証を3段に分ける理由
ここが手順の中心です。Claude Codeが書いた仕様に、わざと2種類の間違いを入れて、各段の反応を比べました。
| ケース | スキーマ検証 | vl-convert | PNG |
|---|---|---|---|
| 正しい仕様 | スキーマ検証エラー0件 | vl-convert変換できる | PNG2本の折れ線 |
markをmrakと綴り間違い | スキーマ検証エラー1件 | vl-convertInvalid specificationで失敗 | PNG出ない |
fieldをsignupと綴り間違い | スキーマ検証エラー0件 | vl-convert変換できる | PNG線が1本もない |
markのようなキー名の誤りは、スキーマ検証とvl-convertの両方が止めてくれます。
厄介なのは3行目です。fieldの値は、スキーマ上はただの文字列なので、データに存在しない列名でも通ります。vl-convertもエラーにせず、PNGを書き出します。
出力されたPNGは空です。Y軸には0の目盛りが1つあるだけで、凡例だけが残ります。コンパイル後のVega仕様を見ると、スケールの定義にもsignupが入ったままでした。つまり「終了コード0 = 正しいグラフ」は成り立ちません。
だから検証は3段にします。
- JSONスキーマ検証で、キー名と値の型の誤りを落とす
fieldがデータの列名と一致するかを突き合わせる- PNGを読んで、線・棒が見えているかを目で確かめる
検証スクリプトchartcheck.pyの全文
3段のうち1段目と2段目、それに3段目で読むPNGの出力までを、tools/chartcheck.py1本にまとめます。1段目はschema/vega-lite-v6.jsonに対するjsonschemaの検証で、導入手順でダウンロードしたスキーマを使います。2段目は、encodingとtransformにあるfieldを集め、データの列名とtransformが作る列(as)を引いた残りを報告します。どこかで落ちたら、終了コード1で止まります。
import json, sys
import jsonschema
import vl_convert as vlc
def fields(node, out):
if isinstance(node, dict):
f = node.get("field")
if isinstance(f, str):
out.add(f)
for v in node.values():
fields(v, out)
elif isinstance(node, list):
for v in node:
fields(v, out)
def made_columns(transforms):
made = set()
for t in transforms:
a = t.get("as") # calculate, bin など
if isinstance(a, str):
made.add(a)
elif isinstance(a, list):
made.update(x for x in a if isinstance(x, str))
for key in ("aggregate", "window"): # 項目ごとの as
for item in t.get(key, []):
if isinstance(item, dict) and isinstance(item.get("as"), str):
made.add(item["as"])
return made
path = sys.argv[1]
spec = json.load(open(path))
# 1段目: JSONスキーマ検証
schema = json.load(open("schema/vega-lite-v6.json"))
errors = list(jsonschema.Draft7Validator(schema).iter_errors(spec))
if errors:
for e in errors[:5]:
print("SCHEMA:", e.message[:200])
sys.exit(1)
# 2段目: field と列名の突き合わせ
have = set().union(*(r.keys() for r in spec["data"]["values"]))
made = made_columns(spec.get("transform", []))
used = set()
fields(spec.get("encoding", {}), used)
fields(spec.get("transform", []), used)
missing = used - have - made
if missing:
print("MISSING:", sorted(missing))
sys.exit(1)
# PNG出力(3段目はこのPNGを人が読む)
out = path.removesuffix(".vl.json") + ".png"
open(out, "wb").write(vlc.vegalite_to_png(spec, scale=2))
print("OK:", out)綴り間違いのfieldを含む仕様に当てるとMISSING: ['signup']と出て、PNGは書かれません。markがmrakの仕様は、1段目のSCHEMA:で止まります。
このスクリプトはdata.values埋め込みの仕様だけを見ます。aggregate・window・calculate・binのasは拾いますが、foldやpivotが作る列は拾いません。使うtransformが増えたら、made_columnsに拾い先を足してください。
typeの取り違えは、線が出ても嘘のグラフになる
fieldが正しくても、typeを取り違えるとグラフは壊れます。同じ正しい仕様からtypeだけを変えた2通りを描きました。どちらもスキーマ検証はエラー0件、突き合わせもMISSINGなしです。
- 日付を
quantitativeにした場合: 折れ線が1本も出ません。X軸にはmonthというラベルだけが残り、目盛りがありません。日付の文字列は数値として読めないためです - 数値を
nominalにした場合: 線は2本とも描かれます。ただしY軸の目盛りは40・55・80・120・150・170の順で等間隔に並び、上が小さく下が大きい逆向きの軸になります
2つ目が危険です。線があるので「描けた」と見えますが、軸の意味が変わっています。この種の誤りは、PNGを読むときに「軸の目盛りが等間隔の数値になっているか」まで確認させないと拾えません。確認項目として、CLAUDE.mdの規約にも1行足しておけます。
- PNGでは、軸の目盛りが値の大きさに比例して並んでいるかも見る型の選び方の目安は、1つだけ覚えておけば足ります。カテゴリはnominal、数値はquantitative、日付はtemporalです。迷う列があるときは、集計前の値を2〜3行見せてClaude Codeに型を理由付きで答えさせると、取り違えが減ります。
transformを使う仕様でも同じ検証で回る
集計やフィルターは、データを書き換えずにtransformで指定できます。Vega-Liteが持つtransformには、Aggregate・Bin・Calculate・Filter・Fold・Pivot・Windowなどがあります。
次は、プランごとの合計を出し、横棒で並べる例です。
{
"$schema": "https://vega.github.io/schema/vega-lite/v6.json",
"width": 300,
"data": {"values": [
{"plan": "Free", "signups": 120},
{"plan": "Free", "signups": 150},
{"plan": "Pro", "signups": 40},
{"plan": "Pro", "signups": 55}
]},
"transform": [
{"aggregate": [{"op": "sum", "field": "signups", "as": "total"}],
"groupby": ["plan"]}
],
"mark": "bar",
"encoding": {
"y": {"field": "plan", "type": "nominal", "sort": "-x"},
"x": {"field": "total", "type": "quantitative"}
}
}集計後の列totalは、asで名前を付けた時点でデータの列になります。突き合わせスクリプトはtotalを既知の列として扱うので、この仕様はMISSINGなしで通り、PNGも書き出せました。
Claude Codeにループを回させる
前節のchartcheck.pyを、仕様ごとに実行します。
.venv/bin/python tools/chartcheck.py charts/signups.vl.jsonあとは、Claude Codeへの指示です。プロジェクトのCLAUDE.mdに、次のような規約を置いておく例です。
## グラフ
- グラフはVega-Liteの仕様JSON(charts/*.vl.json)で書く
- データはdata.valuesに埋め込む。urlは使わない
- 書いたら tools/chartcheck.py を実行し、出力されたPNGを読む
- スクリプトが通っても、線や棒が見えているか目で確かめる
- 見えていなければ、データの列名と field の綴りを疑う画像を渡す方法は、Claude Codeの一般的なワークフローにあります。画像のパスを指定して「この画像を分析して」と頼む方法です。画像は、ターミナルへのドラッグ&ドロップやCtrl+Vの貼り付けでも渡せます。PNGのパスを指示に書いておけば、人が貼り付ける手間がなくなります。
頼み方は、次のように「何を見て直すか」まで書くとループが回ります。
charts/signups.vl.json を、月別・プラン別の折れ線にして。
chartcheck.py を実行し、できたPNGを見て、
線が2本出ているか、軸ラベルが読めるかを確認してから報告して。PNGを読んだ結果は、「描けています」の一言で終わらせないよう、見えているものを具体的に挙げさせると確実です。「Freeは右上がり、Proは…」と書かせれば、空のグラフは通りません。
目視でしか気づけなかった表示崩れ
もう1つ、PNGを見て初めて気づいた例があります。凡例のラベルとタイトルを日本語にした仕様で試しました。
planの値Freeを無料に、タイトルを「月別の新規登録数」に変えた結果は、次のとおりです。
- スキーマ検証はエラー0件、変換も成功
- タイトルの日本語は描かれた
- 凡例の
無料の行が、ラベルなしの丸だけになった
macOSで描画したときの挙動です。検証スクリプトはどちらも通るため、日本語を含むグラフはPNGを読む工程を外せません。
つまずきやすい点
- 相対パスのデータ指定:
data/cars.jsonのような相対パスは、既定でvega-datasetsのCDNから読みにいきます。Claude Codeがサンプルデータ風に書くと、意図せず外部通信が起きます。READMEによれば、ローカルファイルの読み込みは既定でブロックされます。埋め込み方式にしておけば避けられます。 - 信頼できない仕様の変換: 他人から受け取った仕様は、データや画像の読み込み先を指定できます。READMEは、処理前にセキュリティガイドを読むよう案内しています。
$schemaのバージョン: 検証に使うスキーマ(v6)と、仕様の$schemaの宣言は同じバージョンに揃えます。vl-convert-python 1.9.0が受け付けるのは5.8〜6.4のバージョンです。
まとめ
Vega-Liteは、Claude Codeが書くものを1つのJSONに絞れる点が強みです。ただ、スキーマ検証もvl-convertのエラーも、データに存在しないfieldは止めません。列名の突き合わせとPNGの目視を足して、ようやく「描けた」と言えます。
この3段の検証は、Claude Codeに任せられる部分(JSON検証と突き合わせ)と、画像を読ませる部分に分かれます。グラフの代替テキストを付ける工程は、Claudeでグラフの代替テキストと表データを付ける手順が続きます。頼み方の型はClaudeにグラフを作らせる基本の頼み方に、検証コマンドを持たせる考え方はAsyncAPIの仕様書を書かせるスキルにもあります。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。