Files
2026-09-03 12:21:17 +02:00

179 lines
10 KiB
PHP

@props([
'plot', // ['segments','raw','xMax','yMax','coeffs']
'xLabel' => 'Q [m³/h]',
'yLabel' => 'Y',
'interactive' => false, // przeciągalny marker [Q,H]
'id' => 'curve',
'initialQ' => null, // początkowe Q markera
'etaCoeffs' => [], // współczynniki η (do aktualizacji panelu)
'qInput' => null, // selektor pola Q (input)
'hOutput' => null, // selektor wyświetlania H
'etaOutput' => null, // selektor wyświetlania η
'opX' => null, // stały punkt pracy: Q (np. z pomiaru SYNDIS)
'opY' => null, // stały punkt pracy: wartość Y w tym Q
'opColor' => '#c62828', // kolor punktu pracy
'opYLabel' => null, // gdy ustawione - przy Q pokazuje też tę wartość (np. H)
'band' => false, // pionowy pasek podświetlenia przy opX (wykres η)
])
@php
$w = 380; $h = 230; $padL = 48; $padR = 12; $padT = 14; $padB = 30;
$plotW = $w - $padL - $padR; $plotH = $h - $padT - $padB;
$xMax = max(0.0001, (float) $plot['xMax']);
$yMax = max(0.0001, (float) $plot['yMax']);
$sx = fn ($x) => $padL + ($x / $xMax) * $plotW;
$sy = fn ($y) => $padT + $plotH - ($y / $yMax) * $plotH;
$fmt = fn ($v) => $v >= 100 ? number_format($v, 0, ',', ' ')
: ($v >= 10 ? number_format($v, 1, ',', '') : number_format($v, 2, ',', ''));
@endphp
<svg viewBox="0 0 {{ $w }} {{ $h }}" class="w-full h-auto select-none" xmlns="http://www.w3.org/2000/svg"
@if ($interactive)
id="{{ $id }}"
data-padl="{{ $padL }}" data-padt="{{ $padT }}" data-plotw="{{ $plotW }}" data-ploth="{{ $plotH }}"
data-xmax="{{ $xMax }}" data-ymax="{{ $yMax }}"
data-coeffs="{{ json_encode(array_values($plot['coeffs'] ?? [])) }}"
data-etacoeffs="{{ json_encode(array_values($etaCoeffs ?? [])) }}"
data-scope="{{ json_encode($plot['raw']->pluck('x')->values()->all()) }}"
@endif
>
{{-- Siatka + etykiety osi --}}
@for ($g = 0; $g <= 4; $g++)
@php $gy = $padT + $plotH * $g / 4; $vy = $yMax * (1 - $g / 4); @endphp
<line x1="{{ $padL }}" y1="{{ round($gy, 1) }}" x2="{{ $padL + $plotW }}" y2="{{ round($gy, 1) }}" stroke="var(--chart-grid)" stroke-width="1"/>
<text x="{{ $padL - 5 }}" y="{{ round($gy + 3, 1) }}" font-size="8" fill="var(--chart-tick)" text-anchor="end">{{ $fmt($vy) }}</text>
@endfor
@for ($g = 0; $g <= 4; $g++)
@php $gx = $padL + $plotW * $g / 4; $vx = $xMax * $g / 4; @endphp
<line x1="{{ round($gx, 1) }}" y1="{{ $padT }}" x2="{{ round($gx, 1) }}" y2="{{ $padT + $plotH }}" stroke="var(--chart-grid)" stroke-width="1"/>
<text x="{{ round($gx, 1) }}" y="{{ $padT + $plotH + 12 }}" font-size="8" fill="var(--chart-tick)" text-anchor="middle">{{ $fmt($vx) }}</text>
@endfor
{{-- Osie --}}
<line x1="{{ $padL }}" y1="{{ $padT }}" x2="{{ $padL }}" y2="{{ $padT + $plotH }}" stroke="var(--chart-axis)" stroke-width="1"/>
<line x1="{{ $padL }}" y1="{{ $padT + $plotH }}" x2="{{ $padL + $plotW }}" y2="{{ $padT + $plotH }}" stroke="var(--chart-axis)" stroke-width="1"/>
{{-- Krzywa wielomianowa - kolorowe segmenty wg stref Q --}}
@foreach ($plot['segments'] as $seg)
@php $poly = collect($seg['points'])->map(fn ($p) => round($sx($p['x']), 1).','.round($sy($p['y']), 1))->implode(' '); @endphp
<polyline points="{{ $poly }}" fill="none" stroke="{{ $seg['color'] }}" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
@endforeach
{{-- Pasek podświetlenia przy zadanym Q (wykres η) --}}
@if ($band && $opX !== null && $opX >= 0 && $opX <= $xMax)
<rect x="{{ round($sx($opX) - 6, 1) }}" y="{{ $padT }}" width="12" height="{{ $plotH }}" fill="#f4c20d" opacity="0.22"/>
@endif
{{-- Punkty surowe (niebieskie) --}}
@foreach ($plot['raw'] as $p)
<circle cx="{{ round($sx($p->x), 1) }}" cy="{{ round($sy($p->y), 1) }}" r="3" fill="var(--chart-raw)"/>
@endforeach
{{-- Stały punkt pracy (wartości zmierzone) - na rzeczywistej wysokości, nie rzutowany na krzywą --}}
@if ($opX !== null && $opY !== null && $opX >= 0 && $opX <= $xMax)
<circle cx="{{ round($sx($opX), 1) }}" cy="{{ round($sy(max(0, $opY)), 1) }}" r="5" fill="{{ $opColor }}" stroke="#ffffff" stroke-width="1.5"/>
<text x="{{ $padL + $plotW }}" y="{{ $padT + 2 }}" font-size="7" fill="var(--color-secondary)" text-anchor="end">Q = {{ number_format(round((float) $opX, 3), 3, ',', '') }}</text>
@if ($opYLabel !== null)
<text x="{{ $padL + $plotW }}" y="{{ $padT + 11 }}" font-size="7" fill="var(--color-secondary)" text-anchor="end">{{ $opYLabel }} = {{ number_format(round((float) $opY, 1), 1, ',', ' ') }}</text>
@endif
@endif
{{-- Interaktywny marker + tooltip --}}
@if ($interactive)
<g id="{{ $id }}-tip" style="display:none">
<rect id="{{ $id }}-tiprect" rx="3" height="18" fill="#181c1d" opacity="0.9"></rect>
<text id="{{ $id }}-tiptext" font-size="9" fill="#ffffff" text-anchor="middle"></text>
</g>
<circle id="{{ $id }}-marker" r="6" fill="#a61317" stroke="#ffffff" stroke-width="2" style="cursor:grab"></circle>
@endif
{{-- Etykiety osi --}}
<text x="{{ $padL - 4 }}" y="{{ $padT - 4 }}" font-size="9" fill="var(--chart-label)">{{ $yLabel }}</text>
<text x="{{ $padL + $plotW }}" y="{{ $h - 3 }}" font-size="9" fill="var(--chart-label)" text-anchor="end">{{ $xLabel }}</text>
</svg>
@if ($interactive)
<p class="font-body-sm text-body-sm text-on-surface-variant mt-1 text-center">
Przeciągnij <span class="inline-block w-2 h-2 rounded-full align-middle" style="background:#a61317"></span> po wykresie, aby odczytać H dla danego Q.
</p>
<script>
(function () {
function init() {
const svg = document.getElementById(@json($id));
if (!svg) return;
const d = svg.dataset;
const padL = +d.padl, padT = +d.padt, plotW = +d.plotw, plotH = +d.ploth, xMax = +d.xmax, yMax = +d.ymax;
const hC = JSON.parse(d.coeffs || '[]');
const etaC = JSON.parse(d.etacoeffs || '[]');
const poly = (c, x) => c.reduce((s, ci, i) => s + ci * Math.pow(x, i), 0);
const sx = q => padL + (q / xMax) * plotW;
const sy = v => padT + plotH - (Math.max(0, v) / yMax) * plotH;
const marker = document.getElementById(@json($id . '-marker'));
const tip = document.getElementById(@json($id . '-tip'));
const tipR = document.getElementById(@json($id . '-tiprect'));
const tipT = document.getElementById(@json($id . '-tiptext'));
const qInput = @json($qInput) ? document.querySelector(@json($qInput)) : null;
const hOut = @json($hOutput) ? document.querySelector(@json($hOutput)) : null;
const etaOut = @json($etaOutput) ? document.querySelector(@json($etaOutput)) : null;
const fmt = (v, dd) => v.toFixed(dd).replace('.', ',');
// Kolor punktu wg przedziału (strefy Q) - jak segmenty krzywej.
const scope = JSON.parse(d.scope || '[]');
const BAND = { red: '#c62828', amber: '#f9a825', green: '#2e7d32' };
function bandColor(q) {
if (scope.length < 7) return BAND.green;
if (q < scope[2]) return BAND.red;
if (q < scope[3]) return BAND.amber;
if (q < scope[5]) return BAND.green;
if (q < scope[6]) return BAND.amber;
return BAND.red;
}
let hovering = false, dragging = false;
function tipVisible() { tip.style.display = (hovering || dragging) ? '' : 'none'; }
function update(q) {
q = Math.max(0, Math.min(q, xMax));
const hVal = Math.max(0, poly(hC, q));
const mx = sx(q), my = sy(hVal);
marker.setAttribute('cx', mx);
marker.setAttribute('cy', my);
marker.setAttribute('fill', bandColor(q));
const label = 'Q=' + fmt(q, 3) + ' H=' + fmt(hVal, 2);
tipT.textContent = label;
const tw = label.length * 5.2 + 10;
let tx = Math.max(padL, Math.min(mx - tw / 2, padL + plotW - tw));
const ty = Math.max(padT, my - 26);
tipR.setAttribute('x', tx); tipR.setAttribute('y', ty); tipR.setAttribute('width', tw);
tipT.setAttribute('x', tx + tw / 2); tipT.setAttribute('y', ty + 13);
if (qInput) qInput.value = fmt(q, 3);
if (hOut) hOut.textContent = fmt(hVal, 2);
if (etaOut && etaC.length) etaOut.textContent = fmt(Math.max(0, poly(etaC, q)), 2);
}
function clientToQ(e) {
const pt = svg.createSVGPoint();
pt.x = e.clientX; pt.y = e.clientY;
const loc = pt.matrixTransform(svg.getScreenCTM().inverse());
return (loc.x - padL) / plotW * xMax;
}
svg.style.cursor = 'ew-resize';
// Tooltip tylko przy najechaniu na punkt (lub w trakcie przeciągania).
marker.addEventListener('pointerenter', () => { hovering = true; tipVisible(); });
marker.addEventListener('pointerleave', () => { hovering = false; tipVisible(); });
svg.addEventListener('pointerdown', e => { dragging = true; tipVisible(); try { svg.setPointerCapture(e.pointerId); } catch (_) {} update(clientToQ(e)); });
svg.addEventListener('pointermove', e => { if (dragging) update(clientToQ(e)); });
svg.addEventListener('pointerup', () => { dragging = false; tipVisible(); });
svg.addEventListener('pointercancel', () => { dragging = false; tipVisible(); });
// Domyślnie marker w Q_n; tooltip ukryty do najechania.
const initQ = @json($initialQ);
update(initQ === null ? xMax : initQ);
tipVisible();
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script>
@endif