K
KoreUI

Eje temporal

Chart

Pásale objetos DateTime o Carbon, nunca cadenas ya formateadas. La escala se detecta sola, y con ella cada punto cae donde le toca en el calendario — no donde le toca en el array.

Fechas de verdad, no texto

El sensor estuvo caído del 3 al 5 de febrero: esas filas no están en el array. A la izquierda, fechas Carbon de verdad — el eje temporal enseña el hueco y el trazo se PARTE. A la derecha, las mismas fechas pre-formateadas como texto: el gráfico las trata como categorías equiespaciadas y la caída desaparece. Los dos gráficos se ven perfectamente bien. Uno miente.

Fechas de verdad — el hueco se ve

Temperatura del sensor, con el hueco visible
Categoría Temperatura
1 feb. 2026 21,4
2 feb. 2026 22,1
6 feb. 2026 19,8
7 feb. 2026 20,3
8 feb. 2026 21,9

Fechas como texto — el hueco desaparece

Temperatura del sensor, con el hueco escondido
Categoría Temperatura
1 Feb 21,4
2 Feb 22,1
6 Feb 19,8
7 Feb 20,3
8 Feb 21,9

No es una mejora estética: es una corrección de honestidad. Antes había que pre-formatear la fecha en PHP y pasarla como texto. El gráfico la trataba como una categoría, y una categoría se coloca por su ordinal en el array: el 2 y el 6 de febrero salían igual de separados que el 1 y el 2. Con fechas de verdad, el 2 de febrero cae en el 20 % del rango y el 6 en el 100 % — el hueco está ahí porque el hueco existió.

{{-- Basta con pasar objetos DateTime o Carbon: la escala se detecta sola. --}}
<x-kore::chart :data="$sensor" x="medido_en">
    <x-kore::chart.line y="temp" curve="monotone" dots max-gap="2d" />
    <x-kore::chart.axis-x />
    <x-kore::chart.tooltip />
</x-kore::chart>

{{-- Con la fecha ya formateada como texto, el gráfico solo puede colocar los puntos por
     orden. Y si le pides scale="time" con texto en vez de adivinar, lanza una excepción. --}}
<x-kore::chart :data="$sensorTexto" x="dia">
    <x-kore::chart.line y="temp" curve="monotone" dots />
    <x-kore::chart.axis-x />
    <x-kore::chart.tooltip />
</x-kore::chart>
{{-- Basta con pasar objetos DateTime o Carbon: la escala se detecta sola. --}}
<x-kore::chart :data="$sensor" x="medido_en">
    <x-kore::chart.line y="temp" curve="monotone" dots max-gap="2d" />
    <x-kore::chart.axis-x />
    <x-kore::chart.tooltip />
</x-kore::chart>

{{-- Con la fecha ya formateada como texto, el gráfico solo puede colocar los puntos por
     orden. Y si le pides scale="time" con texto en vez de adivinar, lanza una excepción. --}}
<x-kore::chart :data="$sensorTexto" x="dia">
    <x-kore::chart.line y="temp" curve="monotone" dots />
    <x-kore::chart.axis-x />
    <x-kore::chart.tooltip />
</x-kore::chart>

Los ticks caen en el calendario

Noventa días. Con el adelgazado por salto de una escala de bandas saldrían los días 1, 9, 17, 25... — el mismo «1.224» que el eje Y se enorgullece de no mostrar nunca. Aquí el eje elige semanas: todos los ticks son lunes, y la segunda línea de contexto dice el mes, porque un eje del 10 al 20 de enero no diría en ninguna parte de qué mes habla si nadie cae en un día 1.

Visitas diarias durante noventa días
Categoría Visitas
1 ene. 2026 2,4k
2 ene. 2026 2,5k
3 ene. 2026 2,6k
4 ene. 2026 2,6k
5 ene. 2026 2,5k
6 ene. 2026 2,5k
7 ene. 2026 2,5k
8 ene. 2026 2,5k
9 ene. 2026 2,6k
10 ene. 2026 2,7k
11 ene. 2026 2,9k
12 ene. 2026 3,0k
13 ene. 2026 3,2k
14 ene. 2026 3,2k
15 ene. 2026 3,2k
16 ene. 2026 3,1k
17 ene. 2026 2,9k
18 ene. 2026 2,7k
19 ene. 2026 2,4k
20 ene. 2026 2,0k
21 ene. 2026 1,8k
22 ene. 2026 1,6k
23 ene. 2026 1,4k
24 ene. 2026 1,4k
25 ene. 2026 1,4k
26 ene. 2026 1,5k
27 ene. 2026 1,6k
28 ene. 2026 1,7k
29 ene. 2026 1,8k
30 ene. 2026 1,9k
31 ene. 2026 1,9k
1 feb. 2026 1,8k
2 feb. 2026 1,7k
3 feb. 2026 1,7k
4 feb. 2026 1,6k
5 feb. 2026 1,7k
6 feb. 2026 1,8k
7 feb. 2026 2,0k
8 feb. 2026 2,2k
9 feb. 2026 2,5k
10 feb. 2026 2,9k
11 feb. 2026 3,2k
12 feb. 2026 3,4k
13 feb. 2026 3,6k
14 feb. 2026 3,7k
15 feb. 2026 3,7k
16 feb. 2026 3,6k
17 feb. 2026 3,5k
18 feb. 2026 3,3k
19 feb. 2026 3,1k
20 feb. 2026 3,0k
21 feb. 2026 2,9k
22 feb. 2026 2,9k
23 feb. 2026 2,9k
24 feb. 2026 3,0k
25 feb. 2026 3,0k
26 feb. 2026 3,0k
27 feb. 2026 3,0k
28 feb. 2026 2,9k
1 mar. 2026 2,7k
2 mar. 2026 2,5k
3 mar. 2026 2,2k
4 mar. 2026 1,9k
5 mar. 2026 1,7k
6 mar. 2026 1,6k
7 mar. 2026 1,5k
8 mar. 2026 1,5k
9 mar. 2026 1,7k
10 mar. 2026 1,9k
11 mar. 2026 2,1k
12 mar. 2026 2,4k
13 mar. 2026 2,6k
14 mar. 2026 2,8k
15 mar. 2026 2,9k
16 mar. 2026 3,0k
17 mar. 2026 3,0k
18 mar. 2026 3,0k
19 mar. 2026 3,0k
20 mar. 2026 3,0k
21 mar. 2026 3,1k
22 mar. 2026 3,2k
23 mar. 2026 3,4k
24 mar. 2026 3,6k
25 mar. 2026 3,8k
26 mar. 2026 4,1k
27 mar. 2026 4,2k
28 mar. 2026 4,3k
29 mar. 2026 4,3k
30 mar. 2026 4,2k
31 mar. 2026 4,0k

Un eje temporal elige fronteras de calendario — cada hora, cada 6 horas, cada día, cada semana, cada trimestre — nunca «cada N filas». La tabla es la de d3-time, y hay un test de paridad contra un fixture generado con d3-time de verdad: da exactamente los mismos ticks.

<x-kore::chart :data="$trafico" x="dia" height="18rem">
    <x-kore::chart.area y="visitas" curve="monotone" />
    <x-kore::chart.axis-y :ticks="5" format="compact" />
    <x-kore::chart.axis-x />
    <x-kore::chart.tooltip />
</x-kore::chart>
<x-kore::chart :data="$trafico" x="dia" height="18rem">
    <x-kore::chart.area y="visitas" curve="monotone" />
    <x-kore::chart.axis-y :ticks="5" format="compact" />
    <x-kore::chart.axis-x />
    <x-kore::chart.tooltip />
</x-kore::chart>

La unidad y el paso se eligen juntos

Un día muestreado cada media hora. No existe «cada 4 horas» ni «cada 10 minutos»: no son divisores decentes de 24 ni de 60, y los ticks se descuadrarían al día siguiente. La pareja (unidad, paso) se elige junta — al contrario que Chart.js, que fija la unidad y deja el paso en 1: para un eje de un año eso son 365 ticks de un día, escondidos después con autoSkip.

Peticiones por segundo a lo largo del día
Categoría req/s
14 feb. 2026 55
14 feb. 2026, 00:30 51
14 feb. 2026, 01:00 42
14 feb. 2026, 01:30 31
14 feb. 2026, 02:00 24
14 feb. 2026, 02:30 23
14 feb. 2026, 03:00 32
14 feb. 2026, 03:30 50
14 feb. 2026, 04:00 74
14 feb. 2026, 04:30 99
14 feb. 2026, 05:00 119
14 feb. 2026, 05:30 132
14 feb. 2026, 06:00 137
14 feb. 2026, 06:30 136
14 feb. 2026, 07:00 134
14 feb. 2026, 07:30 133
14 feb. 2026, 08:00 140
14 feb. 2026, 08:30 154
14 feb. 2026, 09:00 173
14 feb. 2026, 09:30 196
14 feb. 2026, 10:00 215
14 feb. 2026, 10:30 227
14 feb. 2026, 11:00 230
14 feb. 2026, 11:30 223
14 feb. 2026, 12:00 210
14 feb. 2026, 12:30 195
14 feb. 2026, 13:00 183
14 feb. 2026, 13:30 178
14 feb. 2026, 14:00 179
14 feb. 2026, 14:30 185
14 feb. 2026, 15:00 191
14 feb. 2026, 15:30 194
14 feb. 2026, 16:00 188
14 feb. 2026, 16:30 174
14 feb. 2026, 17:00 152
14 feb. 2026, 17:30 126
14 feb. 2026, 18:00 101
14 feb. 2026, 18:30 82
14 feb. 2026, 19:00 71
14 feb. 2026, 19:30 69
14 feb. 2026, 20:00 72
14 feb. 2026, 20:30 77
14 feb. 2026, 21:00 77
14 feb. 2026, 21:30 72
14 feb. 2026, 22:00 59
14 feb. 2026, 22:30 41
14 feb. 2026, 23:00 23
14 feb. 2026, 23:30 10
<x-kore::chart :data="$peticiones" x="hora">
    <x-kore::chart.line y="rps" curve="monotone" />
    <x-kore::chart.axis-y :ticks="4" />
    <x-kore::chart.axis-x timezone="Europe/Madrid" />
    <x-kore::chart.tooltip />
</x-kore::chart>
<x-kore::chart :data="$peticiones" x="hora">
    <x-kore::chart.line y="rps" curve="monotone" />
    <x-kore::chart.axis-y :ticks="4" />
    <x-kore::chart.axis-x timezone="Europe/Madrid" />
    <x-kore::chart.tooltip />
</x-kore::chart>

La zona horaria no es cosmética

Un pedido de las 23:30 en Madrid es de las 22:30 en UTC — o sea, de OTRO DÍA. Un gráfico diario que lea las fechas en UTC pone ese pedido en la barra de ayer.

La zona vive en el eje, no en el dato
<x-kore::chart.axis-x timezone="Europe/Madrid" />
<x-kore::chart.axis-x timezone="Europe/Madrid" />

Sin timezone, se respeta la zona que traiga el dato. Eloquent entrega las fechas en la zona de la aplicación, así que en el caso normal ya viene bien — pero si lees timestamps en UTC directo de una columna sin castear, esta prop es la que arregla el día en el que cae la barra.

El mismo intervalo agrupa la consulta

Que las escalas vivan en el servidor tiene una consecuencia casi inevitable: el intervalo que decide los ticks es el mismo con el que hay que agrupar el query. Es el $__interval de Grafana, en Eloquent — y no lo tiene nadie más en el ecosistema Laravel.

TimeTicks::interval($desde, $hasta, count: 8) → 1 week
Agrupar el query con el mismo paso
$paso = TimeTicks::interval($desde, $hasta, count: 8);   // → "1 week"

Order::selectRaw('DATE_TRUNC(?, created_at) AS bucket, SUM(total)', [$paso->unit()])
     ->whereBetween('created_at', [$desde, $hasta])
     ->groupBy('bucket')
     ->get();

// $paso->unit()           → 'day' | 'week' | 'month' | ...  (lo que pide un DATE_TRUNC)
// $paso->toDateInterval() → un DateInterval de PHP, por si hay que iterar el rango a mano
$paso = TimeTicks::interval($desde, $hasta, count: 8);   // → "1 week"

Order::selectRaw('DATE_TRUNC(?, created_at) AS bucket, SUM(total)', [$paso->unit()])
     ->whereBetween('created_at', [$desde, $hasta])
     ->groupBy('bucket')
     ->get();

// $paso->unit()           → 'day' | 'week' | 'month' | ...  (lo que pide un DATE_TRUNC)
// $paso->toDateInterval() → un DateInterval de PHP, por si hay que iterar el rango a mano

El tipo importa: TimeTicks::interval() exige DateTimeImmutable en sus dos primeros argumentos. Un \Carbon\Carbon normal (mutable) no vale ahí — hace falta \Carbon\CarbonImmutable o un DateTimeImmutable a secas. Es distinto del x del gráfico, que acepta cualquier DateTimeInterface (mutable o no) porque solo lo lee.

Props de axis-x

Prop Tipo Default Descripción
scale string auto auto detecta fechas y solo fechas. time las exige: con texto, lanza una excepción
timezone string|null la del dato Zona horaria con la que se lee la fecha. Eloquent ya entrega en la de la app
ticks int 6 Objetivo de fronteras de calendario a mostrar. Pista, no contrato
max-labels int 12 Solo aplica a band; en time no hay categorías que saltar
show bool true show=false apaga el eje y su canaleta deja de reservar ancho
Cargando