CluPilotCloud/resources/views/components/ui/spark.blade.php

109 lines
4.9 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

@props([
/** @var array<int, int|float|null> the series, in the order it happened */
'points' => [],
// muted where the figure is observed, accent where it can be acted on.
// Four accent charts in a row is how an accent stops meaning anything.
'tone' => 'accent',
'area' => false,
/*
| The scale. Left null, the line stretches over its own smallest and largest
| value — the classic sparkline, and the right choice when the question is
| "what shape did this take".
|
| It is the WRONG choice when the question is "how busy is this", and the
| host page proved it: a machine sitting at 0.2 % CPU, wobbling between 0.1
| and 0.3, drew a seismograph and flatly contradicted the figure printed
| beside it. Passing min=0 (and max=100 for a percentage) anchors the box so
| a calm host looks calm — the same rule the Chart.js version already
| carried as "an auto-scaled axis makes 3 % look like a wall of load".
*/
'min' => null,
'max' => null,
// Rendered size in px. The viewBox stays 96×34, so this only stretches.
'width' => 80,
'height' => 32,
])
@php
/*
| A missing sample BREAKS the line; it is not filtered out.
|
| This used to start with array_filter(…, 'is_numeric'), which threw the
| gaps away and joined their neighbours. Two things went wrong at once: the
| line claimed a measurement that was never taken, and every point after
| the gap slid left — an hour of samples with two gaps drew itself as
| fifty-eight minutes and put the newest reading somewhere in the middle.
|
| The x position is therefore taken from the point's place in the ORIGINAL
| series, and consecutive readings are collected into segments that are
| drawn as separate paths. A series with nothing missing yields exactly one
| segment and the same picture as before — which is what every existing
| caller passes.
*/
$values = array_values($points);
$numeric = array_values(array_filter($values, 'is_numeric'));
$low = $min ?? ($numeric ? min($numeric) : 0);
$high = $max ?? ($numeric ? max($numeric) : 0);
$span = max(0.0001, $high - $low);
$step = count($values) > 1 ? 96 / (count($values) - 1) : 96;
$segments = [];
$current = [];
foreach ($values as $i => $v) {
if (! is_numeric($v)) {
// A single reading on its own cannot be a line; it ends the segment
// without drawing one, rather than emitting a path of one point.
if (count($current) > 1) {
$segments[] = $current;
}
$current = [];
continue;
}
// Clamped: a value outside the given bounds belongs at the edge, not
// outside the box where it would be clipped into a straight line.
$ratio = min(1, max(0, ((float) $v - $low) / $span));
$current[] = [round($i * $step, 1), round(30 - $ratio * 26, 1)];
}
if (count($current) > 1) {
$segments[] = $current;
}
// Nur die Koordinaten, ohne Befehlsbuchstaben — der Aufrufer setzt das `M`
// oder das `L` davor. Vorher trug diese Funktion ihr `M` selbst, und im
// Flächenpfad stand damit `… L` unmittelbar vor einem `M`: gültig gelesen,
// nicht gezeichnet, und die Füllung verschwand ohne eine einzige Meldung.
$coords = fn (array $seg) => implode(' ', array_map(fn ($p) => $p[0].' '.$p[1], $seg));
// Own id per render: two gradients sharing one would be harmless, but a
// fill referring to a def that a later render replaced would not.
$fade = 'spark-fade-'.substr(md5($tone.serialize($values).$width), 0, 10);
@endphp
@if ($segments !== [])
<svg class="spark {{ $tone === 'accent' ? '' : 'muted' }}" viewBox="0 0 96 34" preserveAspectRatio="none"
style="width:{{ (int) $width }}px;height:{{ (int) $height }}px" aria-hidden="true">
@if ($area)
{{-- A fade rather than a flat tint: the fill carries the eye down to
the baseline instead of ending in a hard edge halfway up. --}}
<defs>
<linearGradient id="{{ $fade }}" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="currentColor" stop-opacity=".22" />
<stop offset="100%" stop-color="currentColor" stop-opacity="0" />
</linearGradient>
</defs>
@endif
@foreach ($segments as $seg)
@if ($area)
{{-- Closed to the baseline under ITS OWN span, not under the whole
box: an area that ran from 0 to 96 would fill the gaps the
line above deliberately leaves open. --}}
<path class="area" fill="url(#{{ $fade }})" d="M{{ $seg[0][0] }} 34 L{{ $coords($seg) }} L{{ end($seg)[0] }} 34 Z" />
@endif
<path class="line" d="M{{ $coords($seg) }}" />
@endforeach
</svg>
@endif