K
KoreUI

Chart - Primeros pasos

Chart

Barras, líneas, áreas y donut, sin ninguna librería de JavaScript. La geometría se calcula en PHP y el servidor devuelve el SVG ya dibujado. Con Livewire, el morph es la actualización.

Los componentes

Un contenedor y varias capas. Cada capa se registra al renderizarse; no dibuja nada por sí sola.

Componente Para qué
<x-kore::chart>El contenedor. Le das los datos y dentro pones las capas
<x-kore::chart.bar>Barras. Con stack se apilan; sin él, se agrupan
<x-kore::chart.line>Una línea
<x-kore::chart.area>Un área bajo la línea
<x-kore::chart.donut>Un donut, o una tarta con inner en cero
<x-kore::chart.axis-y>El eje Y: cuántos ticks y cómo se formatean
<x-kore::chart.axis-x>El eje X: categorías, fechas o números
<x-kore::chart.legend>La leyenda. Son botones: al pulsarlos, ocultan la serie
<x-kore::chart.tooltip>El tooltip y el crosshair

También existen chart.waterfall, chart.gauge, chart.funnel, chart.heatmap, chart.zoom y chart.stream para cascadas, gauges, embudos, mapas de calor, zoom y datos en vivo. Quedan fuera del alcance de esta guía.

Ejemplo completo

No existe un tipo mixto: un gráfico de barras con una línea encima son dos marcas, una detrás de otra. El orden en que las escribes es el orden en que se pintan.

Ingresos y gastos
Ingresos y gastos por mes
Categoría Gastos Ingresos
Ene 800 € 1.240 €
Feb 1.500 € 3.180 €
Mar 1.100 € 2.470 €
Abr 2.200 € 4.910 €
May 1.900 € 4.300 €
Jun 2.600 € 6.120 €
<x-kore::chart :data="$ventas" x="mes" title="Ingresos y gastos">
    <x-kore::chart.bar  y="gastos"   label="Gastos" />
    <x-kore::chart.line y="ingresos" label="Ingresos" curve="monotone" />

    <x-kore::chart.axis-y :ticks="5" format="currency" />
    <x-kore::chart.axis-x />
    <x-kore::chart.legend />
    <x-kore::chart.tooltip />
</x-kore::chart>
<x-kore::chart :data="$ventas" x="mes" title="Ingresos y gastos">
    <x-kore::chart.bar  y="gastos"   label="Gastos" />
    <x-kore::chart.line y="ingresos" label="Ingresos" curve="monotone" />

    <x-kore::chart.axis-y :ticks="5" format="currency" />
    <x-kore::chart.axis-x />
    <x-kore::chart.legend />
    <x-kore::chart.tooltip />
</x-kore::chart>

Cómo funciona

El gráfico lo dibuja el servidor. El SVG llega ya hecho en el HTML de la respuesta.

  • El primer paint es el bueno. No hay un hueco que se rellena cuando arranca el JavaScript.
  • Sin leyenda ni tooltip, no se carga ni un byte de JS. El gráfico funciona igual sin JavaScript.
  • El color nunca es un valor, es un token. Las series se pintan con var(--kore-chart-1), var(--kore-chart-2)… Al cambiar de tema, el gráfico se repinta solo, sin ejecutar nada.
  • El resize tampoco necesita JavaScript. Toda la geometría está en porcentajes del área de trazado, no en píxeles.
  • Los datos van también en una tabla. Un SVG es tan mudo para un lector de pantalla como un canvas. El componente emite además una tabla sr-only con todos los valores, sin costar un byte de JavaScript.

Con Livewire, el morph es la actualización. Cambias el dato en PHP, Livewire morphea el trazo y ya está. Sin wire:ignore, sin chart.update(), sin una instancia de JavaScript que proteger. El morph actualiza la geometría sin recrear el nodo.

Ocultar series con :show

Todo se apaga con la misma prop. Los ejes y la rejilla salen por defecto; el tooltip y la leyenda hay que pedirlos.

Sparkline de ingresos
Categoría Ingresos
Ene 1.240
Feb 3.180
Mar 2.470
Abr 4.910
May 4.300
Jun 6.120

«Gastos» está oculta, pero sigue registrada con el color 1: «Ingresos» conserva el color 2. El color de una serie se asigna por orden de registro, no por orden de dibujo.

<x-kore::chart :data="$ventas" x="mes" :grid="false">
    <x-kore::chart.line y="gastos"   label="Gastos"   :show="false" />
    <x-kore::chart.line y="ingresos" label="Ingresos" curve="monotone" />

    <x-kore::chart.axis-y :show="false" />
    <x-kore::chart.axis-x :show="false" />
</x-kore::chart>
<x-kore::chart :data="$ventas" x="mes" :grid="false">
    <x-kore::chart.line y="gastos"   label="Gastos"   :show="false" />
    <x-kore::chart.line y="ingresos" label="Ingresos" curve="monotone" />

    <x-kore::chart.axis-y :show="false" />
    <x-kore::chart.axis-x :show="false" />
</x-kore::chart>

Por qué no una condición de Blade

Ocultar una marca envolviéndola en una condición no es lo mismo que apagarla con :show.

Si envuelves una marca en una condición y esta falla, la marca desaparece del árbol de Blade: la siguiente hereda su color y todas las series de detrás se recolocan. El lector, que ya sabía que «Ingresos» era la naranja, se encuentra otra cosa naranja. Con :show="false" la marca se registra, se queda con su color y no se dibuja. Las de detrás no se enteran.

Mal: la marca desaparece del árbol
{{-- Si $verGastos es false, «Ingresos» hereda el color 1 y el lector cree ver otra cosa. --}}
@if($verGastos)
    <x-kore::chart.line y="gastos" label="Gastos" />
@endif
<x-kore::chart.line y="ingresos" label="Ingresos" />
{{-- Si $verGastos es false, «Ingresos» hereda el color 1 y el lector cree ver otra cosa. --}}
@if($verGastos)
    <x-kore::chart.line y="gastos" label="Gastos" />
@endif
<x-kore::chart.line y="ingresos" label="Ingresos" />
Bien: la marca se registra y conserva su color
<x-kore::chart.line y="gastos"   label="Gastos"   :show="$verGastos" />
<x-kore::chart.line y="ingresos" label="Ingresos" />
<x-kore::chart.line y="gastos"   label="Gastos"   :show="$verGastos" />
<x-kore::chart.line y="ingresos" label="Ingresos" />

Lo que sí desaparece con :show="false": el trazo, su entrada en la leyenda, su copia en el payload del tooltip, su columna en la tabla accesible, y su aportación al dominio del eje. Si ocultas todas las series, sale el estado vacío.

Los colores

Ocho colores de datos, de chart-1 a chart-8, asignados por orden de escritura de las marcas. La primera marca es la 1, la segunda la 2, y así.

chart-1
chart-2
chart-3
chart-4
chart-5
chart-6
chart-7
chart-8

Son una escala aparte de los tokens semánticos, y a propósito: el token success significa que algo va bien. Si lo usas para la serie 2, le estás diciendo al lector que la serie 2 va bien. Puedes forzar un color concreto cuando la serie sí significa algo, con la prop color="destructive".

La novena serie no existe. La paleta no se cicla: repetir el color de la serie 1 en la novena es peor que no pintarla, porque el lector deja de poder distinguirlas. Con más de ocho series, agrupa el resto en «Otros». Si lo intentas, el componente lanza una excepción.

Instalación

Los componentes vienen con la librería. Dos cosas, como el resto de KoreUi.

1. El paquete de scripts en el layout, solo hace falta si usas leyenda o tooltip:

resources/views/layouts/app.blade.php
<body>
    {{-- tu contenido --}}

    @livewireScripts
    @koreScripts
</body>
<body>
    {{-- tu contenido --}}

    @livewireScripts
    @koreScripts
</body>

2. Que Tailwind vea las vistas del paquete, en tu CSS:

resources/css/app.css
@import 'tailwindcss';
@import '../../vendor/kore-ui/kore-ui/resources/css/kore-theme.css';

@source '../../vendor/kore-ui/kore-ui/resources/**/*.blade.php';
@import 'tailwindcss';
@import '../../vendor/kore-ui/kore-ui/resources/css/kore-theme.css';

@source '../../vendor/kore-ui/kore-ui/resources/**/*.blade.php';

Props del contenedor

Prop Tipo Default Descripción
data array [] Las filas. Arrays, objetos o modelos Eloquent
x string null La clave del eje X. Sin ella, se usa el índice de la fila
height string 16rem Longitud CSS. Se ignora si pones aspect
aspect string null Proporción CSS, por ejemplo 16/9. Alternativa a height
title string null Título visible sobre el gráfico
ariaLabel string null El caption de la tabla accesible. Si no, se usa el título
id string null Id del gráfico. Por defecto, uno determinista por petición
grid bool true La rejilla horizontal, a la altura de los ticks del eje Y
orientation string vertical vertical u horizontal. Transpone las barras; sólo funciona con marcas bar

Configuración

Personaliza los defaults en config/kore-ui.php, sección chart.

config/kore-ui.php
'chart' => [
    'height' => '16rem',        // longitud CSS. Con la prop aspect, se ignora
    'ticks' => 5,                // una pista, no un contrato
    'bar_padding' => 0.2,        // hueco entre barras, como proporción de la banda
    'max_x_labels' => 12,        // tope de etiquetas en un eje de categorías
    'x_ticks' => 6,               // objetivo de ticks en un eje continuo (fechas o números)
    'table_max_rows' => 500,     // tope de la tabla accesible
    'donut_highlight' => true,   // al posarte en un arco, se enciende su fila de la leyenda

    'empty_text' => 'No hay datos que mostrar',
    'empty_icon' => 'chart-line',

    'format' => [
        'decimal_separator' => ',',
        'thousands_separator' => '.',
        'currency' => '€',
        'currency_after' => true,
    ],
],
'chart' => [
    'height' => '16rem',        // longitud CSS. Con la prop aspect, se ignora
    'ticks' => 5,                // una pista, no un contrato
    'bar_padding' => 0.2,        // hueco entre barras, como proporción de la banda
    'max_x_labels' => 12,        // tope de etiquetas en un eje de categorías
    'x_ticks' => 6,               // objetivo de ticks en un eje continuo (fechas o números)
    'table_max_rows' => 500,     // tope de la tabla accesible
    'donut_highlight' => true,   // al posarte en un arco, se enciende su fila de la leyenda

    'empty_text' => 'No hay datos que mostrar',
    'empty_icon' => 'chart-line',

    'format' => [
        'decimal_separator' => ',',
        'thousands_separator' => '.',
        'currency' => '€',
        'currency_after' => true,
    ],
],
Cargando