Datos en vivo
ChartEl morph de Livewire ya ES la actualización. Lo que hay que construir no es refrescar: es saber cuándo NO hacerlo.
Requiere Livewire. <x-kore::chart.stream> conduce un wire:poll disfrazado: necesita un componente Livewire con un método que traiga el dato nuevo (o que lo calcule en render()).
Demo en vivo
Un punto cada 2 segundos. Pon el ratón encima del primer gráfico y el dato se para —mientras lees un tooltip, el número no puede cambiar bajo el cursor—; cambia de pestaña y también se para; y si amplías con el zoom, también. El segundo gráfico (barras) sí anima: no tiene trazo del que despegarse.
En vivo — un punto cada 2 s
0 refrescos
| Categoría | CPU % | Memoria % |
|---|---|---|
| 30 ago. 2026, 11:54:54 | 64 | 59 |
| 30 ago. 2026, 11:54:56 | 72 | 58 |
| 30 ago. 2026, 11:54:58 | 76 | 60 |
| 30 ago. 2026, 11:55 | 63 | 58 |
| 30 ago. 2026, 11:55:02 | 69 | 53 |
| 30 ago. 2026, 11:55:04 | 67 | 55 |
| 30 ago. 2026, 11:55:06 | 76 | 53 |
| 30 ago. 2026, 11:55:08 | 64 | 55 |
| 30 ago. 2026, 11:55:10 | 65 | 56 |
| 30 ago. 2026, 11:55:12 | 73 | 55 |
| 30 ago. 2026, 11:55:14 | 64 | 55 |
| 30 ago. 2026, 11:55:16 | 67 | 51 |
| 30 ago. 2026, 11:55:18 | 73 | 54 |
| 30 ago. 2026, 11:55:20 | 61 | 46 |
| 30 ago. 2026, 11:55:22 | 63 | 49 |
| 30 ago. 2026, 11:55:24 | 53 | 49 |
| 30 ago. 2026, 11:55:26 | 52 | 54 |
| 30 ago. 2026, 11:55:28 | 53 | 47 |
| 30 ago. 2026, 11:55:30 | 41 | 51 |
| 30 ago. 2026, 11:55:32 | 37 | 47 |
| 30 ago. 2026, 11:55:34 | 37 | 54 |
| 30 ago. 2026, 11:55:36 | 37 | 48 |
| 30 ago. 2026, 11:55:38 | 35 | 49 |
| 30 ago. 2026, 11:55:40 | 39 | 47 |
| 30 ago. 2026, 11:55:42 | 33 | 50 |
| 30 ago. 2026, 11:55:44 | 17 | 52 |
| 30 ago. 2026, 11:55:46 | 32 | 48 |
| 30 ago. 2026, 11:55:48 | 14 | 55 |
| 30 ago. 2026, 11:55:50 | 29 | 55 |
| 30 ago. 2026, 11:55:52 | 17 | 49 |
| 30 ago. 2026, 11:55:54 | 20 | 51 |
| 30 ago. 2026, 11:55:56 | 9 | 53 |
| 30 ago. 2026, 11:55:58 | 12 | 50 |
| 30 ago. 2026, 11:56 | 23 | 54 |
| 30 ago. 2026, 11:56:02 | 11 | 55 |
| 30 ago. 2026, 11:56:04 | 21 | 53 |
| 30 ago. 2026, 11:56:06 | 16 | 53 |
| 30 ago. 2026, 11:56:08 | 23 | 61 |
| 30 ago. 2026, 11:56:10 | 20 | 59 |
| 30 ago. 2026, 11:56:12 | 33 | 56 |
Pon el ratón encima y el dato se para. Mientras lees un tooltip, el número que estás mirando no cambia debajo del cursor. Se reanuda al salir. Cambia de pestaña y también se para: diez pestañas abiertas serían diez renders cada dos segundos en tu servidor, para nadie.
Peticiones por endpoint — aquí SÍ se anima, y por eso mismo
| Categoría | req/s |
|---|---|
| /api/pedidos | 120 |
| /api/clientes | 84 |
| /api/pagos | 61 |
| /api/stock | 38 |
| /webhooks | 17 |
Las categorías no se mueven: sólo los valores. No hay ningún trazo del que despegarse, así que las barras viajan a su altura nueva en vez de aparecer en ella. Sólo lo vertical se anima; lo horizontal salta.
Uso básico
Un método que añade un punto nuevo y recorta la ventana deslizante — eso es todo el «streaming».
class Monitor extends Component
{
public array $muestras = [];
/** Un punto nuevo, y el más viejo se cae. Eso es toda la ventana deslizante. */
public function tick(): void
{
$this->muestras[] = $this->leer();
$this->muestras = array_slice($this->muestras, -40);
}
}class Monitor extends Component
{
public array $muestras = [];
/** Un punto nuevo, y el más viejo se cae. Eso es toda la ventana deslizante. */
public function tick(): void
{
$this->muestras[] = $this->leer();
$this->muestras = array_slice($this->muestras, -40);
}
}<x-kore::chart :data="$this->serie" x="medido_en">
<x-kore::chart.line y="cpu" curve="monotone" dots />
<x-kore::chart.axis-y :min="0" :max="100" />
<x-kore::chart.tooltip />
<x-kore::chart.stream every="2s" call="tick" />
</x-kore::chart><x-kore::chart :data="$this->serie" x="medido_en">
<x-kore::chart.line y="cpu" curve="monotone" dots />
<x-kore::chart.axis-y :min="0" :max="100" />
<x-kore::chart.tooltip />
<x-kore::chart.stream every="2s" call="tick" />
</x-kore::chart>Y ya está.
Lo que NO hay aquí
Ni wire:ignore, ni chart.update(), ni una instancia de JavaScript que proteger.
Ni instancia que proteger
El morph cambia el atributo d del <path> sin recrear el nodo. No hay instancia que destruir, así que no hay nada que parpadee. Es el issue #20103 de Filament («el gráfico parpadea en cada polling»), abierto porque cada refresco destruye y recrea una instancia de Chart.js. Nosotros no podemos tener ese bug.
Ni ring buffer, ni motor
La ventana deslizante es un array_slice. El dato ya está en PHP: no hay que mandarlo a ninguna parte para recortarlo.
Ni un byte más de JavaScript
Todo el streaming son ~25 líneas: un temporizador y tres motivos para no dispararlo.
Los cuatro momentos en que refrescar es hostil
Un wire:poll a secas refresca siempre. Por eso el refresco lo conduce el gráfico.
- Mientras lees un tooltip. El dato se movería bajo el cursor y el número que estabas mirando cambiaría mientras lo miras. Pon el ratón encima y el gráfico se para; sácalo y se reanuda.
- Con la pestaña oculta. Diez pestañas abiertas serían diez renders cada dos segundos en tu servidor, para nadie.
- Con el zoom puesto. Has ampliado para mirar algo concreto: que se te mueva el suelo debajo es exactamente lo que no quieres.
- Mientras alguien lee OTRO gráfico del mismo componente Livewire. Es la pausa nº 1 vista desde el otro lado — y no se ve venir.
Un refresco actualiza el componente ENTERO
No un gráfico suelto. De ahí salen dos cosas que hay que saber.
Si pones dos gráficos con stream en el mismo componente Livewire, sólo uno conduce. Dos temporizadores pidiendo exactamente lo mismo serían el doble de round-trips contra el servidor y el dato avanzando al doble de velocidad. El gráfico se da cuenta solo y elige uno: los demás conservan sus transiciones y sus pausas, pero no su propio temporizador.
Y el ratón sobre uno para el refresco de todos. Si comparten componente, comparten dato: un temporizador que dispare mientras lees el gráfico de al lado le movería los números bajo el cursor.
Huecos de verdad: max-gap
Un null explícito ya partía la línea. Lo que NO partía nada era una fila que sencillamente no está.
<x-kore::chart.line y="cpu" curve="monotone" max-gap="10s" /><x-kore::chart.line y="cpu" curve="monotone" max-gap="10s" />Sin eso, si el colector se muere media hora, la línea cruza el hueco dibujando una curva suave por encima de un rato en el que no hubo dato. Y con curve="monotone" se inventa, además, un swoop que parece dato.
Es la misma mentira que arregló el eje temporal, un piso más arriba: entonces el hueco desaparecía porque los puntos se colocaban por su ordinal; ahora el hueco se ve, pero el trazo lo tapa.
max-gap acepta «500ms», «30s», «5m», «2h», «7d», «1w» o un número (las unidades del eje). Sólo tiene sentido en un eje continuo: en uno de categorías no hay huecos que medir, y pedirlo ahí lanza.
Fija el eje Y
En un gráfico en vivo esto deja de ser un lujo.
<x-kore::chart.axis-y :min="0" :max="100" /><x-kore::chart.axis-y :min="0" :max="100" />Un eje que se reescala cada dos segundos porque el dato subió un punto es ilegible: la línea se queda quieta y lo que se mueve es el eje. Un porcentaje va de 0 a 100 y punto.
min y max son un contrato, no una sugerencia: no se redondea por debajo de lo que pides.
Con trazo, nada se anima
Y no es un olvido: es la única respuesta coherente.
La línea es un <path>, y animarla exigiría transition: d. Medido en los tres motores con Playwright:
| Motor | CSS.supports('d') |
¿Interpola de verdad? |
|---|---|---|
| Chromium | true | No — salto seco |
| Firefox | true | Sí |
| WebKit (Safari) | false | Ni lo soporta |
Y hay una razón mejor para no hacerlo aunque funcionara: en una ventana deslizante, interpolar d lleva el punto i hasta el valor del punto i+1 — o sea, la onda tiembla en el sitio en vez de desplazarse. Es la animación equivocada. Un motor de canvas tiene el mismo problema y lo resuelve igual: redibujando.
Eso no es teórico: con los puntos animados, el peor caso medido se iba a 8,36% del área de la curva sobre la que se supone que está — unos 24px en un gráfico de 18rem. Se veía a la legua.
O se anima todo, o no se anima nada. Y como el trazo no puede, no se anima nada. Así que las transiciones sólo se encienden cuando el gráfico no tiene línea ni área. Un gráfico de barras en vivo sí las tiene: ahí no hay ningún trazo del que despegarse.
Y sólo se anima lo vertical
Las etiquetas del eje X no se animan: saltan a su sitio nuevo. Así que una barra que se desplazara despacio hacia la izquierda se quedaría atrás de su propio tick. Lo horizontal se mueve todo junto y de golpe. Lo vertical —que es donde está el dato— desliza.
Internamente, cada barra lleva un wire:key por categoría cuando hay stream: sin él, en una ventana deslizante el morph reutilizaría la barra i para el dato i+1, y la barra crecería en el sitio en vez de que la de al lado se desplace. Es exactamente el temblor que esta regla evita — no hace falta que lo declares tú, lo hace el componente.
Todo esto se apaga con :transition="false", y se respeta prefers-reduced-motion sin que hagas nada.
El techo, y no es negociable
Medido en la demo (40 puntos, 2 series, un componente Livewire con poco más que el gráfico).
| HTML en crudo | 44 kB |
| Comprimido (gzip) | 5,1 kB |
| A 1 Hz | 5,0 kB/s por cliente |
| A 0,2 Hz (5s, el defecto de Filament) | 1,0 kB/s por cliente |
Un refresco es un round-trip completo de Livewire —consulta, Blade, morph— y cuesta entre 30 y 80ms de servidor más la red. Por debajo de medio segundo los refrescos se solapan, Livewire los encola, y el gráfico va cada vez más por detrás mientras el servidor arde. Por eso every lanza si le pides menos.
Y el cuello de botella real no es ése: es el HTML. Son N renders para N clientes. Cincuenta usuarios mirando el mismo panel a 1 Hz son cincuenta renders por segundo — de exactamente el mismo gráfico.
El techo honesto es 1 Hz con ≤ 200 puntos. El defecto sensato son 5s.
A 10 Hz no aguanta ninguna arquitectura que dibuje en el servidor. Ni ésta, ni ninguna. Y decirlo aquí es más útil que dejar que lo descubras en producción.
Si necesitas más de 1 Hz
Lo que hay que cambiar no es el número: es el formato de cable.
La tentación es «pues que dibuje el JavaScript» — o sea, portar Ticks, Scales, Path y Format a JS, y mantener dos implementaciones de la geometría idénticas para siempre. Es exactamente la deuda que esta arquitectura se eligió para no contraer.
Hay una tercera vía, y no la hace nadie del ecosistema: el servidor sigue calculando la geometría, pero deja de mandar HTML y manda la geometría ya resuelta.
Un evento por broadcast (Reverb/Echo) con { d, xTicks, payload }, y unas 30 líneas de JavaScript que hagan path.setAttribute('d', …).
- Cero escalas en JavaScript. Cero
Intl. Cero paridad que mantener. - Y una renderización para N clientes en vez de N renders: es un broadcast, no un poll. A 1 Hz con 50 usuarios,
wire:pollson 50 renders/s; un broadcast es 1.
La única concesión: ese <path> hay que protegerlo del morph (wire:ignore sobre esa capa), porque lo que escribe el cliente, el morph lo borra.
No está implementado. Está aquí porque es la salida correcta, no para fingir que ya existe.
Props
| Prop | Tipo | Default | Descripción |
|---|---|---|---|
every
|
string|int | 5s | Cada cuánto refrescar: «5s», «500ms» o milisegundos. Lanza por debajo de 500ms |
call
|
string|null | null | El método Livewire que trae el dato — como wire:poll.5s=«tick». Sin él, un $refresh() a secas |
transition
|
bool | true | Las marcas se mueven a su sitio nuevo en vez de aparecer en él (solo barras y puntos) |
show
|
bool | true | Apaga el refresco |