/* 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;is.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 && (
)}
setHover(null)}> {/* y grid */} {yTicks.map(t => ( {t} ))} {/* Prisstaplar per kvart, färgade per beslut */} {day.map((s,i) => { const isPast = dayView===0 && i < nowIdx; const isNow = dayView===0 && i === nowIdx; const y = yPrice(s.buy), h = padT+plotH - y; const w = Math.max(1, slotW - 1.4), r = Math.min(3, w/2); const x0 = xAt(i) + 0.7; const col = actColor(mapAction(s.action)); return ; })} {/* SOC-berg (cyan) */} {socArea && } {socLine && } {/* Solproduktion */} {solarArea && } {solarLine && } {/* Förbrukning */} {consLine && } {/* x ticks */} {xTicks.map(h => ( {String(h).padStart(2,'0')}:00 ))} {/* NU-markör (bara idag) */} {dayView===0 && nowIdx!=null && ( )} {/* hover crosshair */} {hover!=null && ( )} {/* 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;