Eje temporal
ChartPá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
| 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
| 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.
| 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.
| 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.
<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.
$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 |