[{"data":1,"prerenderedAt":525},["ShallowReactive",2],{"content-/edinet-quarterly-viewer":3,"all-pages-for-dir":523,"og-image-/edinet-quarterly-viewer":524},{"id":4,"title":5,"body":6,"category":501,"description":502,"extension":503,"meta":504,"navigation":505,"ogImage":506,"path":507,"project_name":508,"published":509,"publishedAt":510,"seo":511,"stem":512,"tags":513,"todo":521,"unpublished":509,"updatedAt":506,"__hash__":522},"pages/2026-04/2026-04-13/edinet-quarterly-viewer.md","EDINET四半期財務データの取得パイプラインとNuxt3チャートUI実装",{"type":7,"value":8,"toc":484},"minimark",[9,13,17,20,25,33,38,41,57,60,63,77,79,83,86,89,92,95,99,102,174,177,180,202,205,216,219,221,224,227,229,233,244,251,394,404,406,409,415,417,420,423,426,433,435,439,446,449,451,454,480],[10,11,5],"h1",{"id":12},"edinet四半期財務データの取得パイプラインとnuxt3チャートui実装",[14,15,16],"p",{},"前日に設計だけで半日溶かした四半期ビューアを、今日は朝から一気に組み上げた。別セッションのClaude CodeにPythonスクリプトでJSONを吐いてもらい、そのデータを受け取って表示側を実装する流れ。途中でユーザーの「会社を切り替えるたびに年次に戻るのが面倒」という一言から、localStorageによるタブ状態永続化まで入れることになった。",[18,19],"hr",{},[21,22,24],"h2",{"id":23},"pythonスクリプトで四半期データを取得変換した","Pythonスクリプトで四半期データを取得・変換した",[14,26,27,28,32],{},"edinet-api側のClaude Codeに、前日作成した指示書を渡してスクリプトを実行してもらった。",[29,30,31],"code",{},"export_quarterly.py"," が完成し、EDINET DB APIから四半期財務データを取得して累計値から単独四半期値への変換とQ4逆算を行い、JSONを出力する。",[34,35,37],"h3",{"id":36},"累計単独四半期の変換ルール","累計→単独四半期の変換ルール",[14,39,40],{},"四半期報告のP/L・CFは累計値で開示される。Q2の売上高は「上半期の合計」であって、Q2単独の数字ではない。これを単独値に変換するには、前四半期の累計値を差し引く。",[42,43,44,48,51,54],"ul",{},[45,46,47],"li",{},"Q1: そのまま（累計 = 単独）",[45,49,50],{},"Q2: Q2累計 - Q1累計",[45,52,53],{},"Q3: Q3累計 - Q2累計",[45,55,56],{},"Q4: 年次値 - Q3累計（年次データから逆算）",[14,58,59],{},"BSはストック値なのでそのまま使う。Q4は四半期報告が存在しないため、年次有報の値から逆算する必要がある。この「Q4逆算」が設計段階でCodexに指摘されたポイントで、テスト時にQ1+Q2+Q3+Q4の合計が年次PLと一致するか検証して全件パスした。",[34,61,62],{"id":62},"再利用可能なパイプラインとして整備",[14,64,65,66,68,69,72,73,76],{},"スクリプト完成後、「次回また同じ手順を踏むのか」という話になった。最初はスキル化を検討したが、",[29,67,31],{}," 自体が ",[29,70,71],{},"uv run python export_quarterly.py"," の1コマンドで済む再利用可能な成果物になっていたので、スキルではなくCLAUDE.mdにワークフローを追記する形に落ち着いた。新しい企業を追加したければ ",[29,74,75],{},"COMPANIES"," 辞書にEDINETコードと期末月を足すだけで動く。",[18,78],{},[21,80,82],{"id":81},"四半期チャートuiをnuxt3で実装した","四半期チャートUIをNuxt3で実装した",[14,84,85],{},"JSONデータを受け取り、表示側の実装に入った。既存の年次データ表示ページに四半期タブを追加する形で進めた。",[34,87,88],{"id":88},"データ構造の確認で計画を修正",[14,90,91],{},"JSONの中身を見て、前日の計画から修正が入った。P/Lは全四半期（Q1-Q4）のデータが揃っていたが、BSはQ4（年次有報由来）のみ、CFデータは四半期にはなかった。当初6チャートの2x3グリッドを想定していたが、データがないものは出せない。BS・CFを外して4チャートの2x2に調整した。",[14,93,94],{},"計画段階でCodexレビューを入れたのは正解だったが、実データの構造は実装時にしか確定しない。「計画で決めた通りに作る」のではなく、データを見てから構成を決める柔軟さが必要だった。",[34,96,98],{"id":97},"年次四半期タブ切替の実装","年次/四半期タブ切替の実装",[14,100,101],{},"最初はチャートパネル（右カラム）だけをタブで切り替える設計にした。しかしユーザーから「ページ全体を切り替えてほしい」とフィードバックが来た。四半期データにはBS詳細やP/L内訳がないので、中央カラムのウォーターフォール・比例縮尺は四半期モードでは出せない。結果として、タブを上部に移動し、年次モードと四半期モードで中央+右カラム全体のレイアウトを切り替える実装にした。",[103,104,109],"pre",{"className":105,"code":106,"language":107,"meta":108,"style":108},"language-vue shiki shiki-themes vitesse-light vitesse-light","\u003C!-- タブを上部に移動し、ページ全体を切り替え -->\n\u003Cdiv class=\"chart-mode-tabs\">\n  \u003Cbutton :class=\"{ active: chartMode === 'annual' }\">年次\u003C/button>\n  \u003Cbutton :class=\"{ active: chartMode === 'quarterly' }\">四半期\u003C/button>\n\u003C/div>\n","vue","",[29,110,111,120,151,158,164],{"__ignoreMap":108},[112,113,116],"span",{"class":114,"line":115},"line",1,[112,117,119],{"class":118},"sxvE3","\u003C!-- タブを上部に移動し、ページ全体を切り替え -->\n",[112,121,123,127,131,135,138,142,146,148],{"class":114,"line":122},2,[112,124,126],{"class":125},"shFtX","\u003C",[112,128,130],{"class":129},"sHkkW","div",[112,132,134],{"class":133},"s4oTP"," class",[112,136,137],{"class":125},"=",[112,139,141],{"class":140},"sMJiu","\"",[112,143,145],{"class":144},"sdGka","chart-mode-tabs",[112,147,141],{"class":140},[112,149,150],{"class":125},">\n",[112,152,154],{"class":114,"line":153},3,[112,155,157],{"class":156},"sG7-3","  \u003Cbutton :class=\"{ active: chartMode === 'annual' }\">年次\u003C/button>\n",[112,159,161],{"class":114,"line":160},4,[112,162,163],{"class":156},"  \u003Cbutton :class=\"{ active: chartMode === 'quarterly' }\">四半期\u003C/button>\n",[112,165,167,170,172],{"class":114,"line":166},5,[112,168,169],{"class":125},"\u003C/",[112,171,130],{"class":129},[112,173,150],{"class":125},[34,175,176],{"id":176},"チャート構成の変遷",[14,178,179],{},"チャートの構成はユーザーのフィードバックで3回変わった。",[181,182,183,190,196],"ol",{},[45,184,185,189],{},[186,187,188],"strong",{},"初期案（4チャート2x2）",": 売上高・営業利益・経常利益・純利益。データ構造を見て決定",[45,191,192,195],{},[186,193,194],{},"営業利益+利益率追加",": 「純利益じゃなくて営業利益にして、営業利益率もつけて」→売上高+営業利益+営業利益率のコンボチャートに変更",[45,197,198,201],{},[186,199,200],{},"純利益復活+1列化",": 「純利益のチャートも結局見えないままだから入れて」「2列じゃなくて1列にして横いっぱいに」",[14,203,204],{},"最終構成は以下の3チャート、1列フル幅レイアウト。",[42,206,207,210,213],{},[45,208,209],{},"売上高+営業利益+営業利益率コンボチャート（棒+折れ線）",[45,211,212],{},"純利益チャート",[45,214,215],{},"総資産チャート",[14,217,218],{},"2列レイアウトだと四半期の20本のバーが窮屈に詰まって読めなかった。1列フル幅にした瞬間、各四半期のバーの間に余白が生まれて数字が追えるようになった。",[18,220],{},[21,222,223],{"id":223},"四半期データテーブルを新規作成した",[14,225,226],{},"チャートだけでなく、数値の一覧テーブルも四半期データに切り替える必要があった。既存の年次テーブルコンポーネントとは構造が異なる（期間ラベルが「FY2024」ではなく「FY2024 Q1」になる等）ため、四半期専用のデータテーブルコンポーネントを新規作成した。親ページで年次/四半期のタブ状態に応じて表示を切り替える。",[18,228],{},[21,230,232],{"id":231},"localstorageでタブ状態を永続化した","localStorageでタブ状態を永続化した",[14,234,235,236,239,240,243],{},"ここが今日の実装で一番「なるほど」と思った変更。会社を切り替えると、サイドバーのリンクで別ページに遷移する。遷移するとVueコンポーネントが再マウントされるので、",[29,237,238],{},"chartMode"," のrefが初期値の ",[29,241,242],{},"'annual'"," に戻る。四半期データを見ていた人が会社Aから会社Bに切り替えると、毎回「年次→四半期」のタブクリックが必要になる。",[14,245,246,247,250],{},"今は2社だから気にならないが、将来500社に増えたら致命的に面倒になる。",[29,248,249],{},"localStorage.setItem('chartMode', 'quarterly')"," で状態を保存し、マウント時に読み出す実装を追加した。",[103,252,256],{"className":253,"code":254,"language":255,"meta":108,"style":108},"language-typescript shiki shiki-themes vitesse-light vitesse-light","// マウント時にlocalStorageから復元\nonMounted(() => {\n  const saved = localStorage.getItem('chartMode')\n  if (saved) chartMode.value = saved\n})\n// タブ切替時に保存\nwatch(chartMode, (val) => localStorage.setItem('chartMode', val))\n","typescript",[29,257,258,263,278,312,338,343,349],{"__ignoreMap":108},[112,259,260],{"class":114,"line":115},[112,261,262],{"class":118},"// マウント時にlocalStorageから復元\n",[112,264,265,269,272,275],{"class":114,"line":122},[112,266,268],{"class":267},"senZ8","onMounted",[112,270,271],{"class":125},"(()",[112,273,274],{"class":125}," =>",[112,276,277],{"class":125}," {\n",[112,279,280,284,287,290,293,296,299,302,305,307,309],{"class":114,"line":153},[112,281,283],{"class":282},"stQ0i","  const ",[112,285,286],{"class":133},"saved",[112,288,289],{"class":125}," =",[112,291,292],{"class":133}," localStorage",[112,294,295],{"class":125},".",[112,297,298],{"class":267},"getItem",[112,300,301],{"class":125},"(",[112,303,304],{"class":140},"'",[112,306,238],{"class":144},[112,308,304],{"class":140},[112,310,311],{"class":125},")\n",[112,313,314,317,320,322,325,328,330,333,335],{"class":114,"line":160},[112,315,316],{"class":129},"  if",[112,318,319],{"class":125}," (",[112,321,286],{"class":133},[112,323,324],{"class":125},")",[112,326,327],{"class":133}," chartMode",[112,329,295],{"class":125},[112,331,332],{"class":133},"value",[112,334,289],{"class":125},[112,336,337],{"class":133}," saved\n",[112,339,340],{"class":114,"line":166},[112,341,342],{"class":125},"})\n",[112,344,346],{"class":114,"line":345},6,[112,347,348],{"class":118},"// タブ切替時に保存\n",[112,350,352,355,357,359,362,364,367,369,371,373,375,378,380,382,384,386,388,391],{"class":114,"line":351},7,[112,353,354],{"class":267},"watch",[112,356,301],{"class":125},[112,358,238],{"class":133},[112,360,361],{"class":125},",",[112,363,319],{"class":125},[112,365,366],{"class":133},"val",[112,368,324],{"class":125},[112,370,274],{"class":125},[112,372,292],{"class":133},[112,374,295],{"class":125},[112,376,377],{"class":267},"setItem",[112,379,301],{"class":125},[112,381,304],{"class":140},[112,383,238],{"class":144},[112,385,304],{"class":140},[112,387,361],{"class":125},[112,389,390],{"class":133}," val",[112,392,393],{"class":125},"))\n",[14,395,396,397,399,400,403],{},"ただしSSR時にlocalStorageは存在しないので、サーバーサイドでは初期値 ",[29,398,242],{}," でレンダリングされ、クライアントで ",[29,401,402],{},"'quarterly'"," に切り替わる。ハイドレーションミスマッチの警告が出るが、表示は正常に動く。",[18,405],{},[21,407,408],{"id":408},"インデックスページを削除してリダイレクトに変更した",[14,410,411,414],{},[29,412,413],{},"/financial-quiz/edinet"," のインデックスページには企業一覧が表示されていたが、「ここは不要、どうせサイドバーで切り替える」とのフィードバックで、最初の企業（freee）への自動リダイレクトに置き換えた。一覧ページの存在意義がなくなっていた。",[18,416],{},[21,418,419],{"id":419},"チャートのゼロ位置揃えで手こずった",[14,421,422],{},"売上高+営業利益+営業利益率のコンボチャートで、左軸（金額、棒グラフ）のゼロ位置と右軸（利益率%、折れ線）のゼロ位置がずれていた。営業利益がマイナスの年があるため、左軸は0よりも下にバーが伸びる。一方、右軸の0%が左軸の0と別の高さにプロットされてしまい、利益率がマイナスなのにグラフ上ではプラス圏にいるように見える。",[14,424,425],{},"修正は、左軸のmin/maxから0の位置（全体に対する比率）を計算し、右軸のスケールを同じ比率で0%が揃うように調整する方式。年次チャートを直した後、四半期チャートにも同じ修正を適用し忘れていてユーザーにスクリーンショットで指摘された。",[14,427,428,429,432],{},"もうひとつ、値ラベルの「万」表記の問題もあった。",[29,430,431],{},"formatYenCompact"," 関数が10,000以上の値を「万」単位に変換していたが、データは既に百万円単位。百万円の値に「万」を掛けて二重に単位変換されていた。百万円の数値をそのまま表示するように修正した。",[18,434],{},[21,436,438],{"id":437},"cfウォーターフォールも追加した","CFウォーターフォールも追加した",[14,440,441,442,445],{},"年次データにはCFの3区分合計（営業CF・投資CF・財務CF）と設備投資額があったので、既存の ",[29,443,444],{},"CFWaterfallChart"," コンポーネントを再利用してキャッシュフローウォーターフォールを追加した。期首現預金から営業CF・設備投資・FCF・その他投資CF・財務CFを経て期末現預金に至る増減を可視化する。",[14,447,448],{},"capexデータがないFY2019以前はウォーターフォールを非表示にする条件分岐を入れた。freeeとマネーフォワードの両社で表示を確認し、数値の整合性もチェックした。",[18,450],{},[21,452,453],{"id":453},"学びメモ",[42,455,456,462,468,474],{},[45,457,458,461],{},[186,459,460],{},"計画はデータの実物を見てから確定する",": 6チャートの構想が、JSONを開いた瞬間に4チャートに変わった。設計段階で考えるのは大事だが、実データの構造が計画を上書きする",[45,463,464,467],{},[186,465,466],{},"「毎回の手間」はユーザー数で掛け算する",": 2社のときは年次→四半期の1クリックが気にならない。500社になったら500クリック。localStorageの数行で将来の500クリックが消えた",[45,469,470,473],{},[186,471,472],{},"両軸チャートのゼロ位置は手動で揃える",": Chart.jsは左右の軸を独立にスケーリングする。マイナス値がある場合、ゼロラインがずれて「プラスに見えるマイナス」が生まれる。スケール計算を自前で書く必要がある",[45,475,476,479],{},[186,477,478],{},"コンポーネント修正は全ての使用箇所に適用する",": 年次チャートのゼロ揃えを直して四半期を忘れた。同じロジックを使う箇所は機械的に洗い出してから修正する",[481,482,483],"style",{},"html pre.shiki code .sxvE3, html code.shiki .sxvE3{--shiki-default:#A0ADA0;--shiki-dark:#A0ADA0}html pre.shiki code .shFtX, html code.shiki .shFtX{--shiki-default:#999999;--shiki-dark:#999999}html pre.shiki code .sHkkW, html code.shiki .sHkkW{--shiki-default:#1E754F;--shiki-dark:#1E754F}html pre.shiki code .s4oTP, html code.shiki .s4oTP{--shiki-default:#B07D48;--shiki-dark:#B07D48}html pre.shiki code .sMJiu, html code.shiki .sMJiu{--shiki-default:#B5695977;--shiki-dark:#B5695977}html pre.shiki code .sdGka, html code.shiki .sdGka{--shiki-default:#B56959;--shiki-dark:#B56959}html pre.shiki code .sG7-3, html code.shiki .sG7-3{--shiki-default:#393A34;--shiki-dark:#393A34}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .senZ8, html code.shiki .senZ8{--shiki-default:#59873A;--shiki-dark:#59873A}html pre.shiki code .stQ0i, html code.shiki .stQ0i{--shiki-default:#AB5959;--shiki-dark:#AB5959}",{"title":108,"searchDepth":122,"depth":122,"links":485},[486,490,495,496,497,498,499,500],{"id":23,"depth":122,"text":24,"children":487},[488,489],{"id":36,"depth":153,"text":37},{"id":62,"depth":153,"text":62},{"id":81,"depth":122,"text":82,"children":491},[492,493,494],{"id":88,"depth":153,"text":88},{"id":97,"depth":153,"text":98},{"id":176,"depth":153,"text":176},{"id":223,"depth":122,"text":223},{"id":231,"depth":122,"text":232},{"id":408,"depth":122,"text":408},{"id":419,"depth":122,"text":419},{"id":437,"depth":122,"text":438},{"id":453,"depth":122,"text":453},"dev","EDINET DB APIから四半期財務データを取得するPythonスクリプトを完成させ、Nuxt3上で年次/四半期タブ切替・コンボチャート・データテーブル・localStorage永続化まで一気に実装した記録","md",{},true,null,"/edinet-quarterly-viewer","financial-data",false,"2026-04-13T00:00:00.000Z",{"title":5,"description":502},"2026-04/2026-04-13/edinet-quarterly-viewer",[514,515,516,517,518,519,520],"EDINET","四半期データ","Nuxt3","Chart.js","localStorage","Python","データパイプライン","memo","XejrDC55iUZJCvQQYOt4m--sGb7VFxoIKZ4b9dx0RQI",[],"https://log.eurekapu.com/og/blog/edinet-quarterly-viewer.png?v=2026-04-13T00%3A00%3A00.000Z&title=EDINET%E5%9B%9B%E5%8D%8A%E6%9C%9F%E8%B2%A1%E5%8B%99%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE%E5%8F%96%E5%BE%97%E3%83%91%E3%82%A4%E3%83%97%E3%83%A9%E3%82%A4%E3%83%B3%E3%81%A8Nuxt3%E3%83%81%E3%83%A3%E3%83%BC%E3%83%88UI%E5%AE%9F%E8%A3%85&author=Kei%20Komatsu&sig=4389eda64a1e29b7",1782528826927]