sheet-charts.js
'use strict';
(() => {
const W=window.WORKSPACE,e=s=>W.ctx.esc(s),numeric=x=>typeof x==='number'&&Number.isFinite(x);
const monthly=[['bar','棒グラフ'],['line','折れ線'],['area','エリア'],['hbar','横棒'],['dot','ドット'],['heat','ヒートマップ'],['table','数値表']].map(([id,name])=>({id,name}));
const short=['立体棒','リング','フロー','マップ','オービット','ドット','バブル','マトリクス','リボン','拡大'];
W.hasLegacyComposition=doc=>{const p=W.period();return doc.id==='sheet-05'&&p.start===p.end&&p.end==='2026-09'&&W.values(doc,p.end).sample;};
W.sheetChartOptions=doc=>W.hasLegacyComposition(doc)?window.STUDIES.charts.map((c,n)=>({id:'legacy-'+c.id,name:short[n],description:c.name})):monthly;
W.sheetChartMode=doc=>{const legacy=W.hasLegacyComposition(doc),options=W.sheetChartOptions(doc),saved=W.state.sheetCharts?.[doc.id]?.[legacy?'legacy':'monthly'],fallback=legacy?'legacy-'+W.ctx.prefs.chart:'bar';return options.find(o=>o.id===saved)||options.find(o=>o.id===fallback)||options[0];};
W.sheetChartPicker=doc=>{const mode=W.sheetChartMode(doc);return `<div class="sheet-graph-row"><span class="control-caption">グラフ</span><div class="sheet-chart-strip" role="group" aria-label="グラフをすぐ切り替える">${W.sheetChartOptions(doc).map(o=>`<button type="button" data-sheet-chart="${o.id}" data-chart-document="${e(doc.id)}" aria-pressed="${mode.id===o.id}" title="${e(o.description||o.name)}">${e(o.name)}</button>`).join('')}</div><span class="chart-strip-note">${W.hasLegacyComposition(doc)?'基準値サンプルの構成':'期間内の月別データ'}</span></div>`;};
W.periodTable=doc=>{const rows=W.periodRows(doc),available=rows.filter(r=>doc.metrics.some(([key])=>numeric(r.data[key])));return `<section class="period-table document-surface"><div class="document-section-title"><h2>期間内の数値一覧</h2><button class="text-button" data-period-csv="${e(doc.id)}">期間一覧をCSV保存</button></div><p class="period-data-note">${e(W.periodLabel())} / ${rows.length}か月中 ${available.length}か月に数値があります。未接続の月は「—」です。</p><div class="period-table-scroll" tabindex="0" role="region" aria-label="月別の数値一覧"><table><caption>${e(doc.title)}・月別数値(合計・平均は行いません)</caption><thead><tr><th scope="col">対象月</th>${doc.metrics.map(([,label,unit])=>`<th scope="col">${e(label)}<small>${e(unit)}</small></th>`).join('')}<th scope="col">区分</th></tr></thead><tbody>${rows.map(r=>`<tr><th scope="row">${e(r.month)}</th>${doc.metrics.map(([key])=>`<td>${e(W.format(r.data[key]))}</td>`).join('')}<td>${e(r.source)}</td></tr>`).join('')}</tbody></table></div></section>`;};
function metricPlot(doc,rows,metric,mode){
const[key,label,unit]=metric,points=rows.map((r,i)=>({i,month:r.month,value:r.data[key]})).filter(p=>numeric(p.value));if(!points.length)return `<article class="monthly-plot"><h3>${e(label)} <small>${e(unit)}</small></h3><p class="empty-explanation">この項目の期間内の数値は未接続です。</p></article>`;
let lo=Math.min(0,...points.map(p=>p.value)),hi=Math.max(0,...points.map(p=>p.value));if(hi===lo)hi=lo+1;const y=v=>225-(v-lo)/(hi-lo)*175,x=i=>rows.length===1?395:90+i/(rows.length-1)*590,zero=y(0),axis=v=>new Intl.NumberFormat('ja-JP',{notation:'compact',maximumFractionDigits:1}).format(v),height=mode==='hbar'?Math.max(220,rows.length*30+65):285;
const valueTitle=p=>`<title>${e(p.month+' / '+label+' / '+W.format(p.value)+' '+unit)}</title>`;
let body='';
if(mode==='heat'){
const min=Math.min(...points.map(p=>p.value)),max=Math.max(...points.map(p=>p.value));return `<article class="monthly-plot"><h3>${e(label)} <small>${e(unit)}</small></h3><div class="monthly-heat">${rows.map(r=>{const v=r.data[key],ok=numeric(v),alpha=ok?(max===min?.55:.14+.76*(v-min)/(max-min)):0;const luma=[25,84,58].map(c=>(c*alpha+255*(1-alpha))/255).map(c=>c<=.04045?c/12.92:((c+.055)/1.055)**2.4).reduce((v,c,i)=>v+c*[.2126,.7152,.0722][i],0);return `<div ${ok?`style="background:rgba(25,84,58,${alpha});color:${luma>.18?'#000':'#fff'}"`:''}><span>${e(r.month)}</span><strong>${e(W.format(v))}</strong></div>`;}).join('')}</div><p class="local-save-note">濃淡は、この項目の期間内の数値の大小を表します。項目間では比較しません。</p></article>`;
}
if(mode==='hbar'){
const xx=v=>160+(v-lo)/(hi-lo)*490,z=xx(0);body=points.map(p=>`<g><text x="80" y="${43+p.i*30}" text-anchor="middle">${e(p.month)}</text><rect x="${Math.min(z,xx(p.value))}" y="${27+p.i*30}" width="${Math.max(1,Math.abs(xx(p.value)-z))}" height="20" rx="3" fill="#295d46">${valueTitle(p)}</rect><text x="${Math.max(165,xx(p.value)+8)}" y="${43+p.i*30}">${e(axis(p.value))}</text></g>`).join('');
}else{
body=[lo,(lo+hi)/2,hi].map(v=>`<line x1="80" x2="700" y1="${y(v)}" y2="${y(v)}" stroke="#dce4dd"/><text x="70" y="${y(v)+5}" text-anchor="end">${e(axis(v))}</text>`).join('');
const labelIndices=new Set([0,rows.length-1,...(rows.length<=6?rows.map((_,n)=>n):[Math.floor(rows.length/2)])]);body+=[...labelIndices].map(n=>`<text x="${x(n)}" y="260" text-anchor="middle">${e(rows[n].month)}</text>`).join('');
if(mode==='bar')body+=points.map(p=>`<rect x="${x(p.i)-Math.min(25,260/rows.length)}" y="${Math.min(zero,y(p.value))}" width="${Math.min(50,520/rows.length)}" height="${Math.max(1,Math.abs(y(p.value)-zero))}" rx="3" fill="#295d46">${valueTitle(p)}</rect>`).join('');
else{if(['line','area'].includes(mode)){let segment=[];const flush=()=>{if(segment.length>1){const d=segment.map((p,n)=>(n?'L':'M')+x(p.i)+' '+y(p.value)).join(' ');if(mode==='area')body+=`<path d="${d} L${x(segment.at(-1).i)} ${zero} L${x(segment[0].i)} ${zero} Z" fill="#295d4628"/>`;body+=`<path d="${d}" stroke="#295d46" stroke-width="3" fill="none"/>`;}segment=[];};for(let n=0;n<rows.length;n++){const p=points.find(p=>p.i===n);if(p)segment.push(p);else flush();}flush();}
body+=points.map(p=>`<circle cx="${x(p.i)}" cy="${y(p.value)}" r="${mode==='dot'?7:5}" fill="#295d46" stroke="#fff" stroke-width="2">${valueTitle(p)}</circle>`).join('');}
}
return `<article class="monthly-plot"><h3>${e(label)} <small>${e(unit)}</small></h3><div class="monthly-plot-scroll" tabindex="0" role="region" aria-label="${e(label)}の月別グラフ"><svg class="monthly-chart" viewBox="0 0 760 ${height}" role="img" aria-label="${e(label+' / '+W.periodLabel()+' / '+unit+'。正確な数値は期間一覧に表示。')}">${body}</svg></div></article>`;
}
W.sheetGraph=doc=>{
const mode=W.sheetChartMode(doc);if(W.hasLegacyComposition(doc))return `<div class="sheet-chart-panel" data-chart-mode="${mode.id}">${W.ctx.dataViz(Number(mode.id.slice(7)),{picker:false})}</div>`;
const rows=W.periodRows(doc),available=rows.filter(r=>doc.metrics.some(([key])=>numeric(r.data[key]))),sample=available.some(r=>r.sample);
const content=!available.length?'<div class="period-empty"><p>この期間のグラフ用データはまだありません。</p><p>数値を入力・取り込むと、選んだ表示に切り替えられます。</p></div>':mode.id==='table'?'<p class="period-data-note">下の「期間内の数値一覧」で、月ごとの数値と区分を確認できます。</p>':`<div class="monthly-plots">${doc.metrics.map(metric=>metricPlot(doc,rows,metric,mode.id)).join('')}</div>`;
return `<section class="sheet-chart-panel document-surface" data-chart-mode="${mode.id}" aria-label="期間内のグラフ"><div class="document-section-title"><h2>${e(mode.name)}</h2><span class="period-data-note">${e(W.periodLabel())}</span></div><p class="period-data-note">${available.length} / ${rows.length}か月に数値あり${sample?' / 検算用サンプルを含む':''}。未接続月を0で補わず、異なる単位は分けて表示します。</p>${content}</section>`;
};
})();