整備工場・車販店 分析グラフ | 開発仕様書

開発者向け(コード構成・データフロー・拡張手順)

リポジトリ配置:/var/www/html/dreampower_graph/
公開URL:http://133.18.180.73/dreampower_graph/
バージョン:ver.14(index.html?v= クエリで管理)
制作者:加瀬

1. アーキテクチャ概要

ファイル構成と責務

ファイル責務
index.htmlDOM骨格。アップロード画面 / ダッシュボード(6タブ)/ 出力中オーバーレイ / 明細モーダル。lib/app.js を読込。
app.js全ロジック。取込・分類・索引構築・各タブ描画・ドリルダウン・出力・取込履歴。約1100行・単一ファイル。
style.cssスタイル全般。
lib/papaparse.min.jsCSVパーサ(PapaParse 5.4.1)
lib/chart.umd.min.jsグラフ描画(Chart.js 4.4.1)
lib/jszip.min.jsZIP生成(JSZip 3.10.1)。PNG一括出力で使用
lib/pptxgen.bundle.jsPowerPoint生成(PptxGenJS 3.12.0)
manual.html / 仕様書.html操作マニュアル(一般向け)/ 本書
make_sample.py動作確認用サンプルCSV生成(Shift-JIS・和暦)。sample/ に出力
外部CDNは使わずライブラリは lib/ に同梱(オフライン動作・依存固定のため)。 ライブラリ更新時はファイル差し替え+動作確認。

2. データフロー

ファイル選択/ドロップ
  → handleFiles()            … 各ファイルを順に処理
  → readFile()               … FileReader で ArrayBuffer 取得
  → decodeBuffer()           … UTF-8 / Shift-JIS を自動判定しテキスト化
  → Papa.parse(header:true)  … 配列<オブジェクト> 化
  → classify()               … ヘッダー列名から 顧客/車両/伝票/入金 を判定
  → state[type] に格納
[分析する]
  → analyze()
      → buildIndexes()       … idx.custTx / idx.vehLast / idx.years を構築
      → safe('タブ名', renderXxx)  … 6タブを個別 try/catch で描画
  → saveSession()            … IndexedDB に取込内容を保存(最新10件)

3. グローバル状態(app.js 冒頭)

変数型 / 内容
state{customers,vehicles,invoices,payments}。各値は {name,rows,fields} または null。rows はパース済み行オブジェクト配列。
idx取込後に buildIndexes() が構築する索引。custTx(顧客CD→{count,total,first,last})/ vehLast(車両CD→最終入庫日)/ years(伝票の年配列)。
chartsChart.js インスタンスのレジストリ。キー=canvas id。draw() が登録、analyze() 冒頭で破棄・クリア。
drillscanvas id → ドリル解決関数 (datasetIndex,index) ⇒ {title,columns,rows}
drillData明細モーダルに現在表示中の {title,columns,rows}。CSV/印刷で参照。
TODAY基準日(new Date() 相当のハードコード)。残日数・活性区分の計算に使用。

4. 入力データの扱い

4.1 文字コード判定 — decodeBuffer()

UTF-8・Shift-JIS の両方で TextDecoder デコードし、日本語文字数から不正文字(U+FFFD)数×20を引いたスコアの高い方を採用。

4.2 ファイル分類 — classify()

ヘッダー列名集合に対し、種別ごとの特徴列(SIGNATURE)の一致数を数え、最大の種別へ割当て(最低2列一致)。

4.3 列名のゆらぎ吸収 — pick(r, ...names)

業務システムの出力により列名が異なるため(例:法人区分 / 法人個人区分)、候補名を並べ最初に値のある列を返す。新たなゆらぎは pick() の候補追加で対応。

4.4 和暦日付 — parseDate()

元号記号(M/T/S/H/R)・漢字元号・西暦を解釈。元号→西暦は ERA_BASE のオフセット表(S=1925, H=1988, R=2018 など)で換算。日が無い H10-01 は1日扱い。日付列は必ず parseDate() を通すこと。

5. 集計の定義

関数 / 定義内容
activityOf(date)活性区分。最終取引からの経過 ≤730日=active / ≤1095日=alert / それ超=inactive。 しきい値変更はこの1関数(車検2年周期を考慮し2年/3年)。
grossProfit(r)伝票1行の粗利=技術料粗利合計+部品粗利合計+諸費用粗利合計+諸費セット粗利合計。
isVehicleSale(r)売上区分名称等に「販売/車販/新車/中古車/納車」を含むか=車販。
isShaken(r)同様に「車検」を含むか。一般整備=!isVehicleSale && !isShaken
車検継続率renderYoY内:車検伝票を車両CD別に日付昇順で並べ、2件目以降を「継続」(isRenewal Set)、初回を「新規」。継続率=継続÷(車両CD付き車検全体)。
areaOf(r)居住エリア。地区があれば優先、無ければ 住所1 から正規表現で都道府県+市区町村郡を抽出。

5.1 RFM 5段階スコア(しきい値はデータから自動算出)

スコアの境界値は固定しない。業態・店舗規模により取引回数や購買額の水準が大きく異なるため、 取込んだCSVの分布から分位数(quintile)で自動算出する。 R・F・M それぞれについて、取引実績のある全顧客の値を昇順に並べ、20/40/60/80パーセンタイル点 (quintileCuts())を境界として5段階に分類する。

スコアR:最終取引からの経過日数
(少ない=高スコア)
F:累計取引回数 / M:累計購買額
(多い=高スコア)
5下位20%(最も最近)上位20%
420〜40%60〜80%
340〜60%40〜60%
260〜80%20〜40%
1上位20%(最も遠い)下位20%
0取引実績なし

R・F・M の定義:R=idx.custTxlast と基準日(TODAY)の差日数、 F=count(伝票件数)、M=total(伝票合計の累計)。顧客⇔伝票は顧客CDで突合。 実装は renderRFM() 内の rCut/fCut/mCut(境界値)と rSc/fSc/mSc(スコア関数)。 RFM内訳グラフの「範囲」列にはこの自動算出された境界値が表示される。

5.2 顧客ランク(★0〜★5)

合計スコア sc = R + F + M(3〜15)で判定。取引実績の無い顧客は sc によらず ★0。 R・F・M が相対評価(分位数)になったため、ランク境界は固定のまま運用できる。

合計スコア sc顧客ランク
13〜15★5
11〜12★4
9〜10★3
7〜8★2
3〜6★1
(取引実績なし)★0

5.3 ランク別の表示指標

指標算出
人数そのランクに属する顧客数(顧客CSV全件が母数。括弧内は構成比)
LTV(累計購買額)ランク内顧客の total の平均
客単価(1伝票あたり)Σtotal ÷ Σcount(取引実績のある顧客のみ)
利用頻度count ÷ 取引年数」の平均(回/年)。取引年数=(初回〜最終取引)/365.25日、最小0.25年)
取引期間初回取引〜最終取引の平均(◯年◯ヶ月)
分位数方式によりスコア境界は店舗ごとのデータに自動追従する(しきい値の手調整は不要)。 分位の刻み(5段階)やランク境界(13/11/9/7)を変える場合は renderRFM() を編集する。

5.4 デモデータ

CSVが無い環境での動作確認用に buildDemoData() を用意。顧客160/車両260/伝票1,600/入金1,300件の 擬似データ(2024〜2026年)を生成して state に直接投入する。アップロード画面の「デモデータで試す」ボタンから実行。 取込履歴には保存しない。

6. 描画レイヤ

主要ヘルパ

kpiRow(parent,items)KPIカード行
chartCard(parent,id,title,sub,opt)カード+canvas生成。PNG保存ボタン付き。canvasのidを返す
tableCard(parent,title,sub,head,rows,opt)表カード。CSV/印刷ボタンを自動付与
noteCard(parent,title,msg,opt)データ欠落時に理由を表示するカード
draw(id,cfg)Chart.js描画。charts[id]へ登録、onClick/onHoverでドリル連動
segBar(id,entries)活性区分つき積み上げ横棒。伝票未取込時は単一系列
tally / sumBy / topEntries / segCounts集計ヘルパ(件数・合計・上位N・活性区分つき件数)

各タブは renderOverview / renderYoY / renderCustomers / renderVehicles / renderRFM / renderRankinganalyze() から safe() 経由で呼ばれ、1タブが例外を投げても他タブは描画される。

Chart.js のアニメーションは analyze()Chart.defaults.animation=false に設定(非表示タブの実寸描画・出力のため)。

7. ドリルダウン

カテゴリ系グラフをクリックすると該当明細をモーダル表示する。

8. 出力機能

9. 取込履歴(IndexedDB)

DB名 dreampower_graph(version 2)。ストアを2つに分離し、件数を増やしても一覧表示が重くならない設計。

ストアkeyPath内容
metaid{id, savedAt, counts}。一覧表示・間引き判定で使用(軽量)
dataid{id, state}。CSV本体。「この内容を表示」時に1件のみ読込

saveSession() が両ストアへ put、getAllKeys()KEEP(=10) 件超を削除。 restoreSession(id) が data から復元し再分析。id は nextId() で単調増加を保証。

10. 拡張ガイド

グラフを1つ追加する

// 該当の renderXxx() 内
chartCard(grid,'my-chart','タイトル','サブ');
draw('my-chart',{type:'bar',data:{labels,datasets},options:{...}});
// 明細ドリルを付ける場合(カテゴリ別グラフ)
drillCat('my-chart', items, r=>r['区分名'], labels, COL_INV, '区分');

変更ポイント早見表

活性区分のしきい値activityOf()(730 / 1095 の日数)
RFMの区分境界recencyBucket / freqBucket / monBucketrScore / fScore / mScore
取込履歴の保持件数KEEP 定数
ファイル判別の特徴列SIGNATURE
列名のゆらぎ追加該当箇所の pick() 候補
明細カラムCOL_CUST / COL_VEH / COL_INV / COL_PAY

デプロイ

  1. app.js / style.css を編集
  2. index.html?v= とフッター ver. を繰上げ(ブラウザキャッシュ対策。必須)
  3. ファイル一式を /var/www/html/dreampower_graph/ へ配置(Apache。AllowOverride None のため .htaccess は無効)

11. 既知の制約