/* plan-chart.jsx — NY Plan-graf (2026-06-04), byggd mot kollegornas målbild.
Lager (botten→topp):
1. Prisstaplar per kvart, färgade efter beslut (ACTION_META = samma som klockan)
2. Batteri-SOC: cyan fylld area (egen 0–100%-skala)
3. Solproduktion: gul fylld area + linje (kW-skala)
4. Förbrukning: vit linje (kW-skala)
Drag/hover → tooltip per kvart. Idag/Imorgon-knappar i headern.
Ingen egen databeräkning — läser slots rakt av. */
const PlanChart = ({ slots, nowIdx, refreshAt }) => {
const [dayView, setDayView] = React.useState(0);
const [hover, setHover] = React.useState(null);
const svgRef = React.useRef(null);
const today = slots.slice(0, 96);
const tomorrow = slots.slice(96, 192);
const hasTomorrow = tomorrow.length === 96 && tomorrow.some(s => s && s.buy != null);
const day = (dayView === 1 && hasTomorrow) ? tomorrow : today;
const META = window.ACTION_META || {};
const mapAction = (a) => (window.hemsAdapter && window.hemsAdapter.mapAction) ? window.hemsAdapter.mapAction(a) : a;
const actColor = (a) => (META[a] && META[a].color) || 'var(--c-wait)';
const W = 1200, H = 360;
const padL = 40, padR = 44, padT = 22, padB = 30;
const plotW = W - padL - padR, plotH = H - padT - padB;
const N = day.length;
const slotW = plotW / N;
const xAt = (i) => padL + i * slotW;
const cxAt = (i) => padL + i * slotW + slotW/2;
const maxPrice = Math.max(50, ...slots.map(s => s.buy || 0)) * 1.05;
const yPrice = (v) => padT + plotH - ((v||0)/maxPrice) * plotH;
const kwMax = Math.max(1, ...day.map(s => Math.max(s.solar||0, s.consumption||0))) * 1.15;
const yKw = (v) => padT + plotH - ((v||0)/kwMax) * plotH;
const ySoc = (p) => padT + plotH - ((p||0)/100) * plotH;
const curve = (vals, yf) => {
if (vals.length < 2) return '';
const p = vals.map((v,i)=>[cxAt(i), yf(v)]);
let d = `M ${p[0][0].toFixed(1)} ${p[0][1].toFixed(1)}`;
for (let i=1;i
s.solar), yKw);
const consLine = curve(day.map(s=>s.consumption), yKw);
const socLine = curve(day.map(s=>s.soc), ySoc);
const areaDown = (line, x0,x1) => line ? line + ` L ${x1.toFixed(1)} ${(padT+plotH).toFixed(1)} L ${x0.toFixed(1)} ${(padT+plotH).toFixed(1)} Z` : '';
const solarArea = areaDown(solarLine, cxAt(0), cxAt(N-1));
const socArea = areaDown(socLine, cxAt(0), cxAt(N-1));
const onMove = (e) => {
const r = svgRef.current.getBoundingClientRect();
const x = ((e.clientX - r.left)/r.width) * W;
const i = Math.max(0, Math.min(N-1, Math.floor((x - padL)/slotW)));
setHover(i);
};
const yTicks = [0, 100, 200].filter(t => t <= maxPrice);
const xTicks = [0,6,12,18,23];
const age = refreshAt != null ? Math.max(0, Math.floor((Date.now()-refreshAt)/1000)) : null;
const sub = age==null ? 'plan' : age<60 ? `uppdaterad ${age}s sedan` : `uppdaterad ${Math.floor(age/60)} min sedan`;
const hv = hover != null ? day[hover] : null;
return (
Plan 24h
{sub}
{hasTomorrow && (
)}
{/* tooltip */}
{hv && (() => {
const r = svgRef.current?.getBoundingClientRect();
const xPx = r ? (cxAt(hover)/W)*r.width : 0;
const right = xPx > (r?r.width:W)*0.6;
const m = META[mapAction(hv.action)] || {};
return (
{String(Math.floor(hover/4)).padStart(2,'0')}:{String((hover%4)*15).padStart(2,'0')} {dayView===1?'imorgon':'idag'}
{m.label||hv.action}
Köp{Math.round(hv.buy)} öre
Sälj{Math.round(hv.sell)} öre
Sol{(hv.solar||0).toFixed(1).replace('.',',')} kW
Förbr.{(hv.consumption||0).toFixed(1).replace('.',',')} kW
{hv.soc!=null &&
SOC{Math.round(hv.soc)}%
}
);
})()}
{/* legend */}
Solproduktion
Batteri-SOC
Förbrukning
{[['ladda_nat','Ladda nät'],['ladda_sol','Ladda sol'],['tack_huset','Täck huset'],['salj_sol','Sälj sol'],['salj_batteri','Export (batteri)'],['peak','Peak shaving'],['vanta','Vänta']].map(([k,l])=>(
META[k] ? {l} : null
))}
);
};
const __pcStyles = document.createElement('style');
__pcStyles.textContent = `
.planchart .pc-wrap{position:relative;padding:14px 18px 6px}
.planchart .pc-svg{display:block;width:100%;height:340px;cursor:crosshair}
.planchart .pc-daytoggle{display:inline-flex;gap:3px;padding:3px;border-radius:999px;background:rgba(0,0,0,.28);border:1px solid var(--line-2)}
.planchart .pc-daytoggle button{appearance:none;border:0;background:transparent;cursor:pointer;font:inherit;font-size:12px;font-weight:500;color:var(--ink-2);padding:5px 14px;border-radius:999px}
.planchart .pc-daytoggle button.on{background:rgba(91,168,255,.18);color:var(--ink)}
.planchart .pc-tip{position:absolute;top:18px;pointer-events:none;background:rgba(14,21,36,.94);border:1px solid var(--line-2);border-radius:11px;padding:10px 13px;min-width:180px;box-shadow:0 14px 36px rgba(0,0,0,.55);backdrop-filter:blur(14px);z-index:5;font-family:Geist}
.planchart .pc-tip .t{font-size:11.5px;color:var(--ink-1);font-weight:500;margin-bottom:6px}
.planchart .pc-tip .r{display:flex;align-items:center;gap:7px;font-size:12px;margin-top:3px}
.planchart .pc-tip .r .d{width:8px;height:8px;border-radius:2px;flex:none}
.planchart .pc-tip .r .k{color:var(--ink-2);flex:1}
.planchart .pc-tip .r .v{color:var(--ink);font-weight:500}
.planchart .pc-legend{display:flex;flex-wrap:wrap;gap:12px 16px;align-items:center;padding:12px 18px 16px;border-top:1px solid var(--line);font-size:11.5px;color:var(--ink-2)}
.planchart .pc-legend .li{display:inline-flex;align-items:center;gap:6px}
.planchart .pc-legend .ln{width:16px;height:2.5px;border-radius:2px;flex:none}
.planchart .pc-legend .sw{width:9px;height:9px;border-radius:2px;flex:none}
.planchart .pc-legend .sep{width:1px;height:14px;background:var(--line)}
`;
document.head.appendChild(__pcStyles);
window.PlanChart = PlanChart;