Claude Media
Claude CodeにVega-Liteのグラフを書かせる — vl-convertでPNG化して確かめる

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-convertPNG
正しい仕様スキーマ検証エラー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段にします。

  1. JSONスキーマ検証で、キー名と値の型の誤りを落とす
  2. fieldがデータの列名と一致するかを突き合わせる
  3. 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の仕様書を書かせるスキルにもあります。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/common-workflows
  2. vega.github.io/vega-lite/docs
  3. vega.github.io/vega-lite/docs/spec.html
  4. github.com/vega/vl-convert
この記事を共有:XはてブLinkedIn