K
KoreUI

Zoom y pan

Chart

El cliente manda dos números. El servidor hace el resto: invierte el dominio, elige los ticks nuevos y reescala el eje Y. Cero escalas portadas a JavaScript.

Requiere Livewire. El zoom guarda su estado —la ventana visible— en un componente Livewire, no en Alpine. <x-kore::chart.zoom> solo funciona dentro de un <livewire:...> con una propiedad pública que reciba wire:model.

Demo en vivo

Arrastra sobre el gráfico para ampliar un tramo. Arrastra la ventana del contexto de abajo para desplazarte (pan). Doble clic —o «Inicio» con foco en la ventana— para volver al año entero. Fíjate en el eje X: el año entero dice trimestres; ampliada una semana, el mismo eje dice días, con sus fronteras de calendario.

Tráfico diario, un año

Visitas y rebotes diarios durante un año
Categoría Visitas Rebotes
1 ene. 2026 2,8k 1,2k
2 ene. 2026 2,8k 1,2k
3 ene. 2026 2,8k 1,2k
4 ene. 2026 2,8k 1,2k
5 ene. 2026 2,8k 1,2k
6 ene. 2026 2,7k 1,2k
7 ene. 2026 2,7k 1,2k
8 ene. 2026 2,7k 1,2k
9 ene. 2026 2,7k 1,2k
10 ene. 2026 2,7k 1,3k
11 ene. 2026 2,9k 1,3k
12 ene. 2026 3,0k 1,4k
13 ene. 2026 3,2k 1,4k
14 ene. 2026 3,3k 1,5k
15 ene. 2026 3,5k 1,5k
16 ene. 2026 3,6k 1,5k
17 ene. 2026 3,7k 1,5k
18 ene. 2026 3,7k 1,5k
19 ene. 2026 3,7k 1,5k
20 ene. 2026 3,6k 1,4k
21 ene. 2026 3,5k 1,4k
22 ene. 2026 3,4k 1,3k
23 ene. 2026 3,2k 1,2k
24 ene. 2026 3,1k 1,2k
25 ene. 2026 2,9k 1,1k
26 ene. 2026 2,8k 1,1k
27 ene. 2026 2,8k 1,1k
28 ene. 2026 2,8k 1,1k
29 ene. 2026 2,8k 1,1k
30 ene. 2026 2,9k 1,1k
31 ene. 2026 2,9k 1,2k
1 feb. 2026 3,0k 1,2k
2 feb. 2026 3,0k 1,3k
3 feb. 2026 3,0k 1,3k
4 feb. 2026 2,9k 1,3k
5 feb. 2026 2,8k 1,3k
6 feb. 2026 2,7k 1,2k
7 feb. 2026 2,5k 1,2k
8 feb. 2026 2,3k 1,1k
9 feb. 2026 2,1k 1k
10 feb. 2026 1,9k 922
11 feb. 2026 1,8k 853
12 feb. 2026 1,7k 799
13 feb. 2026 1,6k 762
14 feb. 2026 1,6k 743
15 feb. 2026 1,6k 741
16 feb. 2026 1,7k 750
17 feb. 2026 1,8k 766
18 feb. 2026 1,9k 782
19 feb. 2026 2,0k 793
20 feb. 2026 2,0k 792
21 feb. 2026 2,1k 779
22 feb. 2026 2,0k 752
23 feb. 2026 2,0k 714
24 feb. 2026 1,9k 670
25 feb. 2026 1,8k 625
26 feb. 2026 1,7k 587
27 feb. 2026 1,6k 562
28 feb. 2026 1,6k 558
1 mar. 2026 1,6k 577
2 mar. 2026 1,6k 622
3 mar. 2026 1,8k 692
4 mar. 2026 1,9k 781
5 mar. 2026 2,1k 885
6 mar. 2026 2,3k 994
7 mar. 2026 2,5k 1,1k
8 mar. 2026 2,7k 1,2k
9 mar. 2026 2,8k 1,3k
10 mar. 2026 2,9k 1,3k
11 mar. 2026 3,0k 1,4k
12 mar. 2026 3,0k 1,4k
13 mar. 2026 3,0k 1,4k
14 mar. 2026 2,9k 1,3k
15 mar. 2026 2,9k 1,3k
16 mar. 2026 8,1k 1,3k
17 mar. 2026 2,9k 1,3k
18 mar. 2026 2,9k 1,3k
19 mar. 2026 3,0k 1,3k
20 mar. 2026 3,1k 1,3k
21 mar. 2026 3,2k 1,3k
22 mar. 2026 3,4k 1,4k
23 mar. 2026 3,6k 1,4k
24 mar. 2026 3,7k 1,5k
25 mar. 2026 3,9k 1,5k
26 mar. 2026 3,9k 1,6k
27 mar. 2026 4,0k 1,6k
28 mar. 2026 4,0k 1,5k
29 mar. 2026 3,9k 1,5k
30 mar. 2026 3,8k 1,5k
31 mar. 2026 3,6k 1,4k
1 abr. 2026 3,5k 1,4k
2 abr. 2026 3,4k 1,3k
3 abr. 2026 3,2k 1,3k
4 abr. 2026 3,2k 1,3k
5 abr. 2026 3,1k 1,3k
6 abr. 2026 3,1k 1,3k
7 abr. 2026 3,2k 1,4k
8 abr. 2026 3,2k 1,4k
9 abr. 2026 3,3k 1,5k
10 abr. 2026 3,3k 1,5k
11 abr. 2026 3,4k 1,5k
12 abr. 2026 3,4k 1,5k
13 abr. 2026 3,3k 1,5k
14 abr. 2026 3,2k 1,5k
15 abr. 2026 3,0k 1,4k
16 abr. 2026 2,8k 1,3k
17 abr. 2026 2,6k 1,2k
18 abr. 2026 2,4k 1,1k
19 abr. 2026 2,3k 992
20 abr. 2026 2,1k 906
21 abr. 2026 2,0k 839
22 abr. 2026 2,0k 794
23 abr. 2026 1,9k 771
24 abr. 2026 2,0k 767
25 abr. 2026 2,1k 780
26 abr. 2026 2,2k 802
27 abr. 2026 2,2k 827
28 abr. 2026 2,3k 849
29 abr. 2026 2,3k 863
30 abr. 2026 2,3k 865
1 may. 2026 2,3k 855
2 may. 2026 2,2k 835
3 may. 2026 2,1k 809
4 may. 2026 2,0k 781
5 may. 2026 1,9k 760
6 may. 2026 1,8k 749
7 may. 2026 1,8k 756
8 may. 2026 1,8k 784
9 may. 2026 1,9k 832
10 may. 2026 2,0k 901
11 may. 2026 2,1k 984
12 may. 2026 2,3k 1,1k
13 may. 2026 2,5k 1,2k
14 may. 2026 2,7k 1,3k
15 may. 2026 2,9k 1,3k
16 may. 2026 3,0k 1,4k
17 may. 2026 3,1k 1,4k
18 may. 2026 3,2k 1,4k
19 may. 2026 3,2k 1,4k
20 may. 2026 3,1k 1,4k
21 may. 2026 3,1k 1,3k
22 may. 2026 3,1k 1,3k
23 may. 2026 3,0k 1,3k
24 may. 2026 3,0k 1,2k
25 may. 2026 3,1k 1,2k
26 may. 2026 3,2k 1,2k
27 may. 2026 3,3k 1,3k
28 may. 2026 3,5k 1,3k
29 may. 2026 3,6k 1,4k
30 may. 2026 3,8k 1,5k
31 may. 2026 4,0k 1,6k
1 jun. 2026 4,1k 1,6k
2 jun. 2026 4,2k 1,7k
3 jun. 2026 4,2k 1,7k
4 jun. 2026 4,2k 1,7k
5 jun. 2026 4,1k 1,7k
6 jun. 2026 4,0k 1,7k
7 jun. 2026 3,9k 1,6k
8 jun. 2026 3,8k 1,6k
9 jun. 2026 3,6k 1,6k
10 jun. 2026 3,5k 1,6k
11 jun. 2026 3,5k 1,5k
12 jun. 2026 3,5k 1,6k
13 jun. 2026 3,5k 1,6k
14 jun. 2026 3,5k 1,6k
15 jun. 2026 3,6k 1,6k
16 jun. 2026 3,7k 1,7k
17 jun. 2026 3,7k 1,7k
18 jun. 2026 3,7k 1,7k
19 jun. 2026 3,7k 1,6k
20 jun. 2026 3,7k 1,6k
21 jun. 2026 3,5k 1,5k
22 jun. 2026 3,4k 1,4k
23 jun. 2026 3,2k 1,3k
24 jun. 2026 3,0k 1,2k
25 jun. 2026 2,8k 1,1k
26 jun. 2026 2,6k 1,0k
27 jun. 2026 2,5k 940
28 jun. 2026 2,4k 884
29 jun. 2026 2,3k 853
30 jun. 2026 2,3k 847
1 jul. 2026 2,3k 864
2 jul. 2026 2,4k 897
3 jul. 2026 2,5k 939
4 jul. 2026 2,6k 984
5 jul. 2026 2,6k 1,0k
6 jul. 2026 2,6k 1,1k
7 jul. 2026 2,6k 1,1k
8 jul. 2026 2,6k 1,1k
9 jul. 2026 2,5k 1,1k
10 jul. 2026 2,4k 1,0k
11 jul. 2026 2,2k 1,0k
12 jul. 2026 2,1k 975
13 jul. 2026 2,1k 956
14 jul. 2026 2,0k 951
15 jul. 2026 2,0k 963
16 jul. 2026 2,1k 993
17 jul. 2026 2,2k 1,0k
18 jul. 2026 2,4k 1,1k
19 jul. 2026 2,5k 1,2k
20 jul. 2026 2,7k 1,2k
21 jul. 2026 2,9k 1,3k
22 jul. 2026 3,1k 1,3k
23 jul. 2026 3,2k 1,4k
24 jul. 2026 3,3k 1,4k
25 jul. 2026 3,3k 1,4k
26 jul. 2026 3,3k 1,4k
27 jul. 2026 3,3k 1,3k
28 jul. 2026 3,3k 1,3k
29 jul. 2026 3,2k 1,3k
30 jul. 2026 3,2k 1,2k
31 jul. 2026 3,2k 1,2k
1 ago. 2026 3,3k 1,3k
2 ago. 2026 3,4k 1,3k
3 ago. 2026 3,5k 1,4k
4 ago. 2026 3,7k 1,4k
5 ago. 2026 3,9k 1,5k
6 ago. 2026 4,1k 1,6k
7 ago. 2026 4,2k 1,7k
8 ago. 2026 4,4k 1,8k
9 ago. 2026 4,4k 1,9k
10 ago. 2026 4,5k 1,9k
11 ago. 2026 4,5k 1,9k
12 ago. 2026 4,4k 1,9k
13 ago. 2026 4,3k 1,9k
14 ago. 2026 4,2k 1,9k
15 ago. 2026 4,0k 1,8k
16 ago. 2026 3,9k 1,8k
17 ago. 2026 3,8k 1,7k
18 ago. 2026 3,8k 1,7k
19 ago. 2026 3,8k 1,7k
20 ago. 2026 3,8k 1,7k
21 ago. 2026 3,9k 1,7k
22 ago. 2026 3,9k 1,7k
23 ago. 2026 4,0k 1,7k
24 ago. 2026 4,1k 1,7k
25 ago. 2026 4,1k 1,7k
26 ago. 2026 4,1k 1,7k
27 ago. 2026 4,0k 1,6k
28 ago. 2026 3,9k 1,6k
29 ago. 2026 3,7k 1,5k
30 ago. 2026 3,5k 1,4k
31 ago. 2026 3,3k 1,3k
1 sep. 2026 3,1k 1,2k
2 sep. 2026 2,9k 1,1k
3 sep. 2026 2,8k 1,1k
4 sep. 2026 2,7k 1,0k
5 sep. 2026 2,7k 1,0k
6 sep. 2026 2,7k 1,0k
7 sep. 2026 2,7k 1,1k
8 sep. 2026 2,8k 1,1k
9 sep. 2026 2,8k 1,2k
10 sep. 2026 2,9k 1,2k
11 sep. 2026 2,9k 1,3k
12 sep. 2026 3,0k 1,3k
13 sep. 2026 2,9k 1,3k
14 sep. 2026 2,9k 1,3k
15 sep. 2026 2,8k 1,3k
16 sep. 2026 2,6k 1,2k
17 sep. 2026 2,5k 1,2k
18 sep. 2026 2,4k 1,1k
19 sep. 2026 2,3k 1,1k
20 sep. 2026 2,2k 1,0k
21 sep. 2026 2,3k 1,0k
22 sep. 2026 2,3k 1,0k
23 sep. 2026 2,4k 1,1k
24 sep. 2026 2,6k 1,1k
25 sep. 2026 2,8k 1,2k
26 sep. 2026 2,9k 1,2k
27 sep. 2026 3,1k 1,3k
28 sep. 2026 3,3k 1,3k
29 sep. 2026 3,4k 1,3k
30 sep. 2026 3,5k 1,4k
1 oct. 2026 3,5k 1,4k
2 oct. 2026 3,5k 1,4k
3 oct. 2026 3,5k 1,3k
4 oct. 2026 3,4k 1,3k
5 oct. 2026 3,4k 1,3k
6 oct. 2026 3,4k 1,3k
7 oct. 2026 3,4k 1,3k
8 oct. 2026 3,5k 1,4k
9 oct. 2026 3,6k 1,5k
10 oct. 2026 3,7k 1,5k
11 oct. 2026 3,9k 1,6k
12 oct. 2026 4,1k 1,7k
13 oct. 2026 4,3k 1,8k
14 oct. 2026 4,5k 1,9k
15 oct. 2026 4,6k 2,0k
16 oct. 2026 4,7k 2,1k
17 oct. 2026 4,7k 2,1k
18 oct. 2026 4,7k 2,1k
19 oct. 2026 4,6k 2,1k
20 oct. 2026 4,5k 2,0k
21 oct. 2026 4,4k 2,0k
22 oct. 2026 4,3k 1,9k
23 oct. 2026 4,2k 1,8k
24 oct. 2026 4,1k 1,8k
25 oct. 2026 4,1k 1,8k
26 oct. 2026 4,1k 1,7k
27 oct. 2026 4,1k 1,7k
28 oct. 2026 4,2k 1,7k
29 oct. 2026 4,3k 1,7k
30 oct. 2026 4,4k 1,8k
31 oct. 2026 4,4k 1,8k
1 nov. 2026 4,5k 1,8k
2 nov. 2026 4,4k 1,7k
3 nov. 2026 4,4k 1,7k
4 nov. 2026 4,2k 1,7k
5 nov. 2026 4,1k 1,6k
6 nov. 2026 3,9k 1,5k
7 nov. 2026 3,7k 1,5k
8 nov. 2026 3,5k 1,4k
9 nov. 2026 3,3k 1,3k
10 nov. 2026 3,2k 1,3k
11 nov. 2026 3,1k 1,3k
12 nov. 2026 3,0k 1,3k
13 nov. 2026 3,0k 1,3k
14 nov. 2026 3,1k 1,4k
15 nov. 2026 3,1k 1,4k
16 nov. 2026 3,2k 1,4k
17 nov. 2026 3,3k 1,5k
18 nov. 2026 3,3k 1,5k
19 nov. 2026 3,3k 1,5k
20 nov. 2026 3,2k 1,5k
21 nov. 2026 3,1k 1,4k
22 nov. 2026 3,0k 1,4k
23 nov. 2026 2,9k 1,3k
24 nov. 2026 2,8k 1,2k
25 nov. 2026 2,6k 1,2k
26 nov. 2026 2,5k 1,1k
27 nov. 2026 2,5k 1,0k
28 nov. 2026 2,5k 1,0k
29 nov. 2026 2,6k 1,0k
30 nov. 2026 2,7k 1,1k
1 dic. 2026 2,8k 1,1k
2 dic. 2026 3,0k 1,2k
3 dic. 2026 3,2k 1,2k
4 dic. 2026 3,3k 1,3k
5 dic. 2026 3,5k 1,3k
6 dic. 2026 3,6k 1,4k
7 dic. 2026 3,7k 1,4k
8 dic. 2026 3,7k 1,5k
9 dic. 2026 3,7k 1,5k
10 dic. 2026 3,6k 1,5k
11 dic. 2026 3,6k 1,5k
12 dic. 2026 3,6k 1,5k
13 dic. 2026 3,5k 1,5k
14 dic. 2026 3,6k 1,5k
15 dic. 2026 3,6k 1,6k
16 dic. 2026 3,8k 1,7k
17 dic. 2026 3,9k 1,7k
18 dic. 2026 4,1k 1,8k
19 dic. 2026 4,3k 1,9k
20 dic. 2026 4,5k 2,0k
21 dic. 2026 4,7k 2,1k
22 dic. 2026 4,8k 2,1k
23 dic. 2026 4,9k 2,2k
24 dic. 2026 4,9k 2,2k
25 dic. 2026 4,9k 2,1k
26 dic. 2026 4,9k 2,1k
27 dic. 2026 4,8k 2,0k
28 dic. 2026 4,7k 2,0k
29 dic. 2026 4,6k 1,9k
30 dic. 2026 4,5k 1,8k
31 dic. 2026 4,4k 1,8k

Arrastra para ampliar. Doble clic para volver.

Ventas semanales, dos años — su ventana es otra

Ventas semanales de dos años
Categoría Ventas
6 ene. 2025 15,6k
13 ene. 2025 16,2k
20 ene. 2025 16,4k
27 ene. 2025 16,4k
3 feb. 2025 16,3k
10 feb. 2025 16,3k
17 feb. 2025 16,5k
24 feb. 2025 17,0k
3 mar. 2025 17,7k
10 mar. 2025 18,7k
17 mar. 2025 19,7k
24 mar. 2025 20,6k
31 mar. 2025 21,3k
7 abr. 2025 21,6k
14 abr. 2025 21,4k
21 abr. 2025 20,8k
28 abr. 2025 20,0k
5 may. 2025 18,9k
12 may. 2025 17,9k
19 may. 2025 17,1k
26 may. 2025 16,5k
2 jun. 2025 16,3k
9 jun. 2025 16,3k
16 jun. 2025 16,5k
23 jun. 2025 16,7k
30 jun. 2025 16,7k
7 jul. 2025 16,5k
14 jul. 2025 16,0k
21 jul. 2025 15,1k
28 jul. 2025 14,1k
4 ago. 2025 13,0k
11 ago. 2025 12,0k
18 ago. 2025 11,3k
25 ago. 2025 11,0k
1 sep. 2025 11,1k
8 sep. 2025 11,6k
15 sep. 2025 12,4k
22 sep. 2025 13,4k
29 sep. 2025 14,3k
6 oct. 2025 15,0k
13 oct. 2025 15,5k
20 oct. 2025 15,6k
27 oct. 2025 15,6k
3 nov. 2025 15,5k
10 nov. 2025 15,4k
17 nov. 2025 15,6k
24 nov. 2025 16,1k
1 dic. 2025 16,9k
8 dic. 2025 18,1k
15 dic. 2025 19,5k
22 dic. 2025 20,9k
29 dic. 2025 22,3k
5 ene. 2026 23,3k
12 ene. 2026 24,0k
19 ene. 2026 24,3k
26 ene. 2026 24,2k
2 feb. 2026 23,8k
9 feb. 2026 23,4k
16 feb. 2026 23,0k
23 feb. 2026 22,8k
2 mar. 2026 22,9k
9 mar. 2026 23,3k
16 mar. 2026 23,8k
23 mar. 2026 24,3k
30 mar. 2026 24,8k
6 abr. 2026 24,9k
13 abr. 2026 24,7k
20 abr. 2026 24,0k
27 abr. 2026 23,1k
4 may. 2026 21,8k
11 may. 2026 20,5k
18 may. 2026 19,4k
25 may. 2026 18,5k
1 jun. 2026 17,9k
8 jun. 2026 17,7k
15 jun. 2026 17,9k
22 jun. 2026 18,2k
29 jun. 2026 18,6k
6 jul. 2026 18,8k
13 jul. 2026 18,8k
20 jul. 2026 18,5k
27 jul. 2026 18,0k
3 ago. 2026 17,4k
10 ago. 2026 16,7k
17 ago. 2026 16,2k
24 ago. 2026 16,1k
31 ago. 2026 16,3k
7 sep. 2026 17,0k
14 sep. 2026 18,0k
21 sep. 2026 19,3k
28 sep. 2026 20,7k
5 oct. 2026 21,9k
12 oct. 2026 22,9k
19 oct. 2026 23,6k
26 oct. 2026 23,9k
2 nov. 2026 24,0k
9 nov. 2026 24,0k
16 nov. 2026 24,0k
23 nov. 2026 24,2k
30 nov. 2026 24,6k
7 dic. 2026 25,4k
14 dic. 2026 26,5k
21 dic. 2026 27,6k
28 dic. 2026 28,8k

Sin slider de contexto. Amplía uno y mira la URL: cada gráfico lleva su propia clave.

Uso básico

El zoom necesita un componente Livewire con una propiedad pública para la ventana. Se recomienda #[Url] para que el zoom se pueda compartir por enlace.

app/Livewire/Trafico.php
use Livewire\Attributes\Url;

class Trafico extends Component
{
    /** El tramo visible, en % del dominio COMPLETO. `null` es todo. */
    #[Url(as: 'v', except: null)]
    public ?array $ventana = null;
}
use Livewire\Attributes\Url;

class Trafico extends Component
{
    /** El tramo visible, en % del dominio COMPLETO. `null` es todo. */
    #[Url(as: 'v', except: null)]
    public ?array $ventana = null;
}
resources/views/livewire/trafico.blade.php
<x-kore::chart :data="$this->trafico" x="dia" :window="$ventana">
    <x-kore::chart.area y="visitas" curve="monotone" />
    <x-kore::chart.axis-x />
    <x-kore::chart.tooltip />

    <x-kore::chart.zoom wire:model.live="ventana" />
</x-kore::chart>
<x-kore::chart :data="$this->trafico" x="dia" :window="$ventana">
    <x-kore::chart.area y="visitas" curve="monotone" />
    <x-kore::chart.axis-x />
    <x-kore::chart.tooltip />

    <x-kore::chart.zoom wire:model.live="ventana" />
</x-kore::chart>

Y ya está. Arrastra sobre el gráfico para ampliar un tramo, arrastra la ventana del contexto para desplazarte, doble clic para volver.

Por qué no hay ni una escala en JavaScript

La ventana son dos porcentajes del dominio completo. No dos fechas, no dos números del dominio: dos porcentajes.

Eso tiene tres consecuencias, y las tres son la razón de que esto no cueste ni una línea de matemática de escalas en el cliente:

  1. Componer un zoom sobre otro es una regla de tres. Arrastras del 20% al 60% de una vista que ya enseña [40, 80], y la ventana nueva es [48, 64]. El cliente no necesita saber qué es una fecha, ni un locale, ni un formato.
  2. Es la misma operación para las tres escalas. Recortar el espacio 0–100 a un tramo es un remapeo afín, y da igual que debajo haya categorías, fechas o números.
  3. El servidor devuelve el gráfico entero, ya resuelto. Invierte el dominio (con LinearScale::invert()), elige los ticks nuevos, reescala el eje Y y calcula el <path>. Livewire lo morphea.

Lo más visible de todo: al ampliar, el eje temporal cambia de unidad solo. Un año dice trimestres; ampliada una semana, el mismo eje dice días — con sus fronteras de calendario, en el idioma de la aplicación y con su línea de contexto. Un zoom en el cliente tendría que recalcular esos ticks: o sea, portar TimeTicks, TimeInterval y TimeFormat a JavaScript.

Todo el JavaScript del zoom son ~60 líneas y 0,7 kB gzip, y no es más que aritmética sobre porcentajes.

El eje Y se reescala sobre lo visible

Ampliar una semana de un año y dejar el eje Y llegando al máximo anual deja el gráfico aplastado contra el suelo: el pico ya no está en pantalla, pero el eje sigue reservándole sitio.

Así que el dominio del eje Y se calcula sobre las filas visibles. Es lo que ECharts llama filterMode: 'filter', y es la decisión de diseño del zoom, no un detalle.

Las filas de fuera no se borran: se quedan con una posición negativa o mayor que 100, para que el trazo siga saliendo por el borde en vez de cortarse en seco contra él. El recorte es visual (clip-path), no de dato.

No te puedes quedar atrapado

Dos cosas, y las dos hacen falta.

1. Hay un suelo para la ventana

Sin él se amplía hasta un tramo más fino que la separación entre dos puntos, y ahí no queda nada que dibujar. El suelo son dos separaciones medias — lo justo para que quepa un segmento de línea o un par de barras— y lo calcula el servidor, porque es el único que sabe cuántas filas hay. Cuando el gesto pide menos, la ventana se ensancha alrededor de su centro en vez de descartarse.

2. El estado vacío conserva los controles

El suelo no garantiza que haya datos: en una serie con un hueco (un sensor caído tres días) puedes ampliar dentro del hueco, y ahí no hay nada — y está bien que así sea, el hueco es real. Lo que no puede pasar es que no haya cómo volver: el estado vacío dice «No hay datos en este tramo» y mantiene el botón de restablecer y el slider de contexto.

Un gráfico puede quedarse sin datos que enseñar. Lo que no puede es quedarse sin salida.

Controles accesibles con teclado

Chips de +, − y Restablecer, y la ventana del contexto es un «button» que entra en el tab order y se desplaza con las flechas.

Ni ECharts, ni uPlot, ni Highcharts tienen un zoom que se pueda usar con el teclado (el mantenedor de uPlot cerró la puerta a la accesibilidad por escrito). El arrastre es el atajo para quien tiene ratón, no el mecanismo.

El slider de contexto

Un segundo «svg» con la serie entera.

Y no es una forma de hablar: un <path> son 17 nodos de DOM pase lo que pase — con diez puntos o con diez mil. En una arquitectura que dibuja en el servidor, un gráfico de contexto no cuesta nada. En una de canvas es un segundo motor de dibujo.

Apagar el slider
<x-kore::chart.zoom wire:model.live="ventana" :slider="false" />
<x-kore::chart.zoom wire:model.live="ventana" :slider="false" />

El estado vive en Livewire

Y eso no es un detalle. Sale gratis:

  • Sobrevive al morph sin ningún hook. Livewire no puede borrar lo que él mismo renderiza.
  • Se comparte por URL. Un #[Url] y ya: ese zoom es un enlace.
  • Se testea sin navegador, con Livewire::test().

Un zoom que viviera en Alpine necesitaría un hook del morph para no perderse, no se podría enlazar, y no se podría testear.

Dos trampas del envoltorio

Dos cosas que hay que saber antes de usar el zoom en un layout real.

1. La raíz del componente necesita w-full si la página es un flex

El zoom obliga a envolver el gráfico en un componente Livewire, y la raíz de un componente Livewire es un <div> más. Si el contenedor de la página es un flex —una tarjeta, una rejilla de KPIs, casi cualquier layout—, un flex item se encoge a su contenido por defecto. El gráfico sí rellena a su padre (lleva width: 100%), pero el padre no rellena la tarjeta. Y como el ancho depende del contenido, baila al ampliar: medido, 822px sin zoom y 737px con zoom, en una tarjeta de 944.

resources/views/livewire/trafico.blade.php
<div class="w-full space-y-3">   {{-- ← la raíz del componente Livewire --}}
    <x-kore::chart :window="$ventana"> … </x-kore::chart>
</div>
<div class="w-full space-y-3">   {{-- ← la raíz del componente Livewire --}}
    <x-kore::chart :window="$ventana"> … </x-kore::chart>
</div>

2. Varios gráficos con zoom en la misma vista: cada uno lleva su propia propiedad

La ventana no es global: es de su gráfico. Dos gráficos son dos propiedades, y dos #[Url] con claves distintas.

#[Url(as: 'trafico', except: null)]
public ?array $ventanaTrafico = null;

#[Url(as: 'ventas', except: null)]
public ?array $ventanaVentas = null;
#[Url(as: 'trafico', except: null)]
public ?array $ventanaTrafico = null;

#[Url(as: 'ventas', except: null)]
public ?array $ventanaVentas = null;

La URL queda ?trafico[0]=20&trafico[1]=45&ventas[0]=60&ventas[1]=90. Ampliar uno no toca al otro. Si les das el mismo as:, se pisan — así que dales claves distintas.

Lo que NO hay: rueda ni pinch

Y no es un olvido.

Zoom continuo con rueda o pinch. Una rueda dispara ~50 eventos por segundo, y cada uno cambia los ticks del eje. Hacerlo bien exige portar Ticks, Scales, Path y Format a JavaScript: unos 5–6kB gzip y, sobre todo, dos implementaciones de la geometría que hay que mantener idénticas para siempre.

Con una trampa concreta: Format tendría que producir byte a byte lo mismo en el ICU de PHP y en el de JavaScript — y ese problema ya nos mordió una vez, con los espacios raros que ICU mete antes del «€».

Es exactamente la deuda que esta arquitectura se eligió para no contraer.

El pan sí cabe, y por un motivo geométrico. Desplazar una ventana no deforma nada: ni el trazo, ni el ancho de una barra, ni un radio, ni una etiqueta. Sólo mueve. Ampliarla, sí deforma. Por eso durante el arrastre la ventana se mueve en el cliente (es compositor puro, sale gratis) y al soltar se pide un round-trip. Es el modelo de un mapa: el mosaico se escala mientras arrastras, y se redibuja nítido al soltar.

Props

Prop Tipo Default Descripción
window array|null null Prop del componente chart contenedor. El tramo visible, en % del dominio completo
wire:model string Prop de chart.zoom. Obligatorio. La propiedad Livewire donde vive la ventana
slider bool true Prop de chart.zoom. El mini-gráfico de contexto
show bool true Prop de chart.zoom. Apaga el zoom

Gestos

Gesto Qué hace
Arrastrar sobre el gráficoAmplía ese tramo
Arrastrar la ventana del contextoSe desplaza (pan)
Arrastrar un asa del contextoEstira o encoge la ventana
Clic en el contexto, fuera de la ventanaLleva la ventana ahí
Doble clic sobre el gráficoRestablece
sobre la ventana (con el foco)Se desplaza
Inicio sobre la ventanaRestablece
Cargando