179 lines
10 KiB
PHP
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
|