@@ -797,19 +797,37 @@ async function loadFinance() {
</div> ` ;
// ── SVG-Balkendiagramm ────────────────────────────────────────
// Br eite skaliert mit der Anzahl Tage (Mindestplatz pro Tag), statt bei vielen
// Tagen alles in eine feste Breite zu quetschen — sonst werden Balken bei
// langen Abrechnungsjahren (>60 Tage) zu Haarlinien und unlesbar .
const H = 180 , PL = 44 , PR = 12 , PT = 10 , PB = 28 ;
const n = days . length ;
const MIN _SLOT = 10 ; // px pro Tag (Balkenpaar + Abstand), unterhalb dessen nichts mehr lesbar ist
// Bei langen Abrechnungsjahren nicht jeden einzelnen Tag zeigen — statt 112
// Tagesbalken lieber ~16 Wochenbalken, deutlich lesbarer und trotzdem
// aussagekräftig für "würde sich Spot lohnen" .
const rawN = days . length ;
const bucket = rawN > 180 ? 14 : rawN > 21 ? 7 : 1 ;
const periods = bucket === 1 ? days . map ( d => ( {
fixed _eur : d . fixed _eur , spot _eur : d . spot _eur , label : d . date . slice ( 5 ) ,
} ) ) : ( ( ) => {
const out = [ ] ;
for ( let i = 0 ; i < days . length ; i += bucket ) {
const chunk = days . slice ( i , i + bucket ) ;
const fixed = chunk . reduce ( ( s , d ) => s + ( d . fixed _eur || 0 ) , 0 ) ;
const spotVals = chunk . map ( d => d . spot _eur ) . filter ( v => v != null ) ;
const spot = spotVals . length ? spotVals . reduce ( ( a , b ) => a + b , 0 ) : null ;
const from = chunk [ 0 ] . date . slice ( 5 ) , to = chunk [ chunk . length - 1 ] . date . slice ( 5 ) ;
out . push ( { fixed _eur : fixed , spot _eur : spot , label : from === to ? from : ` ${ from } – ${ to } ` } ) ;
}
return out ;
} ) ( ) ;
const n = periods . length ;
const H = 190 , PL = 44 , PR = 12 , PT = 14 , PB = 30 ;
const MIN _SLOT = 34 ; // px pro Balkenpaar — bewusst großzügig, jetzt wo wenige Balken übrig sind
const W = Math . max ( 600 , PL + PR + n * MIN _SLOT ) ;
const cW = W - PL - PR ;
const cH = H - PT - PB ;
const barW = Math . max ( 3 , Math . floor ( cW / n ) - 2 ) ;
const gap = Math . max ( 1 , Math . floor ( cW / n ) - barW ) ;
const slot = cW / n ;
const barW = Math . min ( 22 , Math . max ( 4 , slot * 0.34 ) ) ;
const barGap = Math . max ( 2 , slot * 0.06 ) ;
const allVals = days . flatMap ( d => [ d . fixed _eur , d . spot _eur ] . filter ( v => v != null ) ) ;
const allVals = periods . flatMap ( d => [ d . fixed _eur , d . spot _eur ] . filter ( v => v != null ) ) ;
const maxVal = Math . max ( ... allVals , 0.01 ) ;
const yTicks = 4 ;
@@ -817,40 +835,52 @@ async function loadFinance() {
for ( let i = 0 ; i <= yTicks ; i ++ ) {
const v = maxVal * i / yTicks ;
const y = PT + cH - ( cH * i / yTicks ) ;
gridLines += ` <line x1=" ${ PL } " y1=" ${ y . toFixed ( 1 ) } " x2=" ${ W - PR } " y2=" ${ y . toFixed ( 1 ) } " stroke=" ${ C . grid } " stroke-width="0.5 "/> ` ;
yLabels += ` <text x=" ${ PL - 4 } " y=" ${ ( y + 4 ) . toFixed ( 1 ) } " text-anchor="end" font-size="9 " fill="#8b949e"> ${ fEur ( v ) } </text> ` ;
gridLines += ` <line x1=" ${ PL } " y1=" ${ y . toFixed ( 1 ) } " x2=" ${ W - PR } " y2=" ${ y . toFixed ( 1 ) } " stroke=" ${ C . grid } " stroke-width="1" stroke-dasharray="2,4 "/> ` ;
yLabels += ` <text x=" ${ PL - 8 } " y=" ${ ( y + 3 ) . toFixed ( 1 ) } " text-anchor="end" font-size="10 " fill="#8b949e"> ${ fEur ( v ) } </text> ` ;
}
// Label-Abstand an der tatsächlichen Balkenbreite ausrichten (~60px pro Label),
// statt pauschal 8 Labels über die ganze — jetzt variable — Breite zu verteilen.
const labelStep = Math . max ( 1 , Math . round ( 60 / ( barW + gap ) ) ) ;
const labelStep = Math . max ( 1 , Math . round ( 50 / slot ) ) ;
const rBar = Math . min ( 3 , barW / 2 ) ;
let bars = '' , xLabels = '' ;
days . forEach ( ( d , i ) => {
const x0 = PL + i * ( barW + gap ) ;
const hFixed = cH * d . fixed _eur / maxVal ;
bars += ` <rect x=" ${ x0 } " y=" ${ ( PT + cH - hFixed ) . toFixed ( 1 ) } " width=" ${ barW / 2 } " height=" ${ hFixed . toFixed ( 1 ) } " fill =" ${ C . fixed } " opacity="0.85 "/> ` ;
periods . forEach ( ( d , i ) => {
const cx = PL + i * slot + slot / 2 ;
const hFixed = Math . max ( 0 , cH * d . fixed _eur / maxVal ) ;
bars += ` <rect x=" ${ ( cx - barGap / 2 - barW ) . toFixed ( 1 ) } " y=" ${ ( PT + cH - hFixed ) . toFixed ( 1 ) } " width=" ${ barW . toFixed ( 1 ) } " height=" ${ Math . max ( hFixed , 1 ) . toFixed ( 1 ) } " rx =" ${ rBar } " fill="url(#gradFixed) "/> ` ;
if ( d . spot _eur != null ) {
const hSpot = cH * d . spot _eur / maxVal ;
bars += ` <rect x=" ${ x0 + barW / 2 } " y=" ${ ( PT + cH - hSpot ) . toFixed ( 1 ) } " width=" ${ barW / 2 } " height=" ${ hSpot . toFixed ( 1 ) } " fill =" ${ C . spot } " opacity="0.85 "/> ` ;
const hSpot = Math . max ( 0 , cH * d . spot _eur / maxVal ) ;
bars += ` <rect x=" ${ ( cx + barGap / 2 ) . toFixed ( 1 ) } " y=" ${ ( PT + cH - hSpot ) . toFixed ( 1 ) } " width=" ${ barW . toFixed ( 1 ) } " height=" ${ Math . max ( hSpot , 1 ) . toFixed ( 1 ) } " rx =" ${ rBar } " fill="url(#gradSpot) "/> ` ;
}
if ( i % labelStep === 0 ) {
const label = d . date . slice ( 5 ) ;
xLabels += ` <text x=" ${ ( x0 + barW / 2 ) . toFixed ( 1 ) } " y=" ${ H - 4 } " text-anchor="middle" font-size="9" fill="#8b949e"> ${ label } </text> ` ;
xLabels + = ` <text x=" ${ cx . toFixed ( 1 ) } " y=" ${ H - 6 } " text-anchor="middle" font-size="9" fill="#8b949e"> ${ d . label } </text> ` ;
}
} ) ;
const legend = ` <text x=" ${ PL } " y=" ${ H + 18 } " font-size="10" fill=" ${ C . fixed } ">■ Festpreis</text >
<text x=" ${ PL + 70 } " y=" ${ H + 18 } " font-size ="1 0" fill=" ${ C . spot } ">■ Spot (hypothetisch)</text> ` ;
const defs = ` <defs >
<linearGradient id="gradFixed" x1="0" y1 ="0" x2="0" y2="1">
<stop offset="0%" stop-color=" ${ C . fixed } " stop-opacity="0.95"/>
<stop offset="100%" stop-color=" ${ C . fixed } " stop-opacity="0.55"/>
</linearGradient>
<linearGradient id="gradSpot" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color=" ${ C . spot } " stop-opacity="0.95"/>
<stop offset="100%" stop-color=" ${ C . spot } " stop-opacity="0.55"/>
</linearGradient>
</defs> ` ;
const bucketLabel = bucket === 1 ? 'Tag' : bucket === 7 ? 'Woche' : '2 Wochen' ;
const legend = ` <div style="display:flex;gap:18px;margin-top:10px;font-size:11px;color:var(--text-dim)">
<span style="display:inline-flex;align-items:center;gap:6px"><span style="width:10px;height:10px;border-radius:3px;background: ${ C . fixed } ;display:inline-block"></span>Festpreis</span>
<span style="display:inline-flex;align-items:center;gap:6px"><span style="width:10px;height:10px;border-radius:3px;background: ${ C . spot } ;display:inline-block"></span>Spot (hypothetisch)</span>
</div> ` ;
const chartSection = `
<div style="border-top:1px solid var(--border);padding-top:24px;margin-top:4px">
<div style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim);margin-bottom:14px">Kosten je Tag </div>
<div style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim);margin-bottom:14px">Kosten je ${ bucketLabel } </div>
<div style="overflow-x:auto">
<svg viewBox="0 0 ${ W } ${ H + 24 } " width=" ${ W } " height=" ${ H + 24 } " style="width: ${ W } px;max-width:none;display:block">
${ gridLines } ${ yLabels } ${ bars } ${ xLabels } ${ legend }
<svg viewBox="0 0 ${ W } ${ H } " width=" ${ W } " height=" ${ H } " style="width: ${ W } px;max-width:none;display:block">
${ defs } ${ gridLines } ${ yLabels } ${ bars } ${ xLabels }
</svg>
</div>
${ n > 60 ? ` <div style="font-size:11px;color:var(--text-dim);margin-top:6px">← Horizontal scrollbar, ${ n } Tage</div> ` : '' }
${ legend }
</div> ` ;
el . innerHTML = ` <div style="padding:4px 0"> ${ empfehlung } ${ cards } ${ chartSection } </div> ` ;