Gráficos y Estadísticas chartsvgzero-dependencywafflepercentagepart-to-wholegrid
Waffle Chart
Cuadrícula de 10×10 donde cada casilla es un 1% del total; visión intuitiva de porcentajes.
Vista Previa en Vivo
Each square = 1% of sessions
- 54% Desktop
- 38% Mobile
- 8% Tablet
View data
| Part | Share |
|---|---|
| Desktop | 54% |
| Mobile | 38% |
| Tablet | 8% |
Uso
Copia y pega este código directamente en tu página de Astro:
astro
---
import { WaffleChart } from 'astro-component-kit/charts';
---
<WaffleChart
parts={[
{ label: 'Desktop', value: 54 },
{ label: 'Mobile', value: 38 },
{ label: 'Tablet', value: 8 },
]}
/> ---
import { WaffleChart } from 'astro-component-kit/charts';
---
<WaffleChart
parts={[
{ label: 'Desktop', value: 54 },
{ label: 'Mobile', value: 38 },
{ label: 'Tablet', value: 8 },
]}
/> Instalación Manual
Si no estás usando el paquete npm, crea un archivo nuevo y pega el siguiente código: src/components/lib/WaffleChart.astro:
astro
---
/**
* WaffleChart — 10×10 grid of squares where each square is 1% of the whole.
* An intuitive part-to-whole view for percentages.
*
* @param {Array<{label: string, value: number}>} parts - Parts of the whole (rounded to 100 cells).
* @param {number} columns - Cells per row (default 10).
* @param {string[]} colors - Optional per-part colors.
* @param {string} variant - Card style (glass|solid|ghost).
* @param {string} size - Density (sm|md|lg).
*/
import ChartFrame from './ChartFrame.astro';
import { colorAt, sum } from './chart-utils';
interface Props {
title?: string;
subtitle?: string;
parts?: Array<{ label: string; value: number }>;
columns?: number;
colors?: string[];
variant?: 'glass' | 'solid' | 'ghost';
size?: 'sm' | 'md' | 'lg';
className?: string;
}
const {
title = 'Device share',
subtitle = 'Each square = 1% of sessions',
parts = [
{ label: 'Desktop', value: 54 },
{ label: 'Mobile', value: 38 },
{ label: 'Tablet', value: 8 },
],
columns = 10,
colors,
variant = 'glass',
size = 'md',
className = '',
} = Astro.props;
const total = sum(parts.map((p) => p.value)) || 1;
// Largest-remainder rounding so cells always add up to exactly 100.
const raw = parts.map((p) => (p.value / total) * 100);
const counts = raw.map(Math.floor);
let remaining = 100 - sum(counts);
raw
.map((v, i) => ({ i, r: v - Math.floor(v) }))
.sort((a, b) => b.r - a.r)
.forEach(({ i }) => {
if (remaining > 0) {
counts[i] += 1;
remaining -= 1;
}
});
const cells: number[] = counts.flatMap((c, i) => Array.from({ length: c }, () => i));
const items = parts.slice(0, 8).map((p, i) => ({ ...p, pct: counts[i], color: colorAt(i, colors) }));
---
<ChartFrame
title={title}
subtitle={subtitle}
variant={variant}
size={size}
className={className}
table={{ headers: ['Part', 'Share'], rows: items.map((p) => [p.label, `${p.pct}%`]) }}
>
<div class="wf">
<div class="wf__grid" style={`--wf-cols:${columns}`} role="img" aria-label={`${title}: ${items.map((p) => `${p.label} ${p.pct}%`).join(', ')}`}>
{cells.map((pi, idx) => (
<span class="wf__cell" style={`background:${items[pi]?.color}; --wf-i:${idx}`} data-tip-title={items[pi]?.label} data-tip={`${items[pi]?.color}::${items[pi]?.pct}%`}></span>
))}
</div>
<ul class="wf__legend">
{items.map((p) => (
<li>
<span class="wf__big">{p.pct}<small>%</small></span>
<span class="wf__name"><i style={`background:${p.color}`} aria-hidden="true"></i>{p.label}</span>
</li>
))}
</ul>
</div>
</ChartFrame>
<style>
.wf {
display: grid;
grid-template-columns: minmax(0, 260px) 1fr;
gap: 2rem;
align-items: center;
}
.wf__grid {
display: grid;
grid-template-columns: repeat(var(--wf-cols), 1fr);
gap: 3px;
}
.wf__cell {
aspect-ratio: 1;
border-radius: 3px;
animation: wf-in 0.4s ease both;
animation-delay: calc(var(--wf-i) * 6ms);
}
.wf__cell:hover { filter: brightness(1.25); }
@keyframes wf-in { from { opacity: 0; transform: scale(0.4); } }
.wf__legend { list-style: none; margin: 0; padding: 0; display: grid; gap: 1rem; }
.wf__legend li { display: flex; flex-direction: column; }
.wf__big {
font-size: 1.75rem;
font-weight: 750;
color: var(--c-text-1, #fff);
line-height: 1.1;
font-variant-numeric: tabular-nums;
}
.wf__big small { font-size: 0.9rem; color: var(--c-text-2, #a3a3a3); margin-left: 2px; }
.wf__name { display: flex; align-items: center; gap: 0.45rem; font-size: 0.8125rem; color: var(--c-text-2, #a3a3a3); }
.wf__name i { width: 10px; height: 10px; border-radius: 3px; }
@media (max-width: 560px) {
.wf { grid-template-columns: 1fr; }
.wf__legend { grid-template-columns: repeat(auto-fit, minmax(90px, 1fr)); }
}
@media (prefers-reduced-motion: reduce) { .wf__cell { animation: none; } }
</style> ---
/**
* WaffleChart — 10×10 grid of squares where each square is 1% of the whole.
* An intuitive part-to-whole view for percentages.
*
* @param {Array<{label: string, value: number}>} parts - Parts of the whole (rounded to 100 cells).
* @param {number} columns - Cells per row (default 10).
* @param {string[]} colors - Optional per-part colors.
* @param {string} variant - Card style (glass|solid|ghost).
* @param {string} size - Density (sm|md|lg).
*/
import ChartFrame from './ChartFrame.astro';
import { colorAt, sum } from './chart-utils';
interface Props {
title?: string;
subtitle?: string;
parts?: Array<{ label: string; value: number }>;
columns?: number;
colors?: string[];
variant?: 'glass' | 'solid' | 'ghost';
size?: 'sm' | 'md' | 'lg';
className?: string;
}
const {
title = 'Device share',
subtitle = 'Each square = 1% of sessions',
parts = [
{ label: 'Desktop', value: 54 },
{ label: 'Mobile', value: 38 },
{ label: 'Tablet', value: 8 },
],
columns = 10,
colors,
variant = 'glass',
size = 'md',
className = '',
} = Astro.props;
const total = sum(parts.map((p) => p.value)) || 1;
// Largest-remainder rounding so cells always add up to exactly 100.
const raw = parts.map((p) => (p.value / total) * 100);
const counts = raw.map(Math.floor);
let remaining = 100 - sum(counts);
raw
.map((v, i) => ({ i, r: v - Math.floor(v) }))
.sort((a, b) => b.r - a.r)
.forEach(({ i }) => {
if (remaining > 0) {
counts[i] += 1;
remaining -= 1;
}
});
const cells: number[] = counts.flatMap((c, i) => Array.from({ length: c }, () => i));
const items = parts.slice(0, 8).map((p, i) => ({ ...p, pct: counts[i], color: colorAt(i, colors) }));
---
<ChartFrame
title={title}
subtitle={subtitle}
variant={variant}
size={size}
className={className}
table={{ headers: ['Part', 'Share'], rows: items.map((p) => [p.label, `${p.pct}%`]) }}
>
<div class="wf">
<div class="wf__grid" style={`--wf-cols:${columns}`} role="img" aria-label={`${title}: ${items.map((p) => `${p.label} ${p.pct}%`).join(', ')}`}>
{cells.map((pi, idx) => (
<span class="wf__cell" style={`background:${items[pi]?.color}; --wf-i:${idx}`} data-tip-title={items[pi]?.label} data-tip={`${items[pi]?.color}::${items[pi]?.pct}%`}></span>
))}
</div>
<ul class="wf__legend">
{items.map((p) => (
<li>
<span class="wf__big">{p.pct}<small>%</small></span>
<span class="wf__name"><i style={`background:${p.color}`} aria-hidden="true"></i>{p.label}</span>
</li>
))}
</ul>
</div>
</ChartFrame>
<style>
.wf {
display: grid;
grid-template-columns: minmax(0, 260px) 1fr;
gap: 2rem;
align-items: center;
}
.wf__grid {
display: grid;
grid-template-columns: repeat(var(--wf-cols), 1fr);
gap: 3px;
}
.wf__cell {
aspect-ratio: 1;
border-radius: 3px;
animation: wf-in 0.4s ease both;
animation-delay: calc(var(--wf-i) * 6ms);
}
.wf__cell:hover { filter: brightness(1.25); }
@keyframes wf-in { from { opacity: 0; transform: scale(0.4); } }
.wf__legend { list-style: none; margin: 0; padding: 0; display: grid; gap: 1rem; }
.wf__legend li { display: flex; flex-direction: column; }
.wf__big {
font-size: 1.75rem;
font-weight: 750;
color: var(--c-text-1, #fff);
line-height: 1.1;
font-variant-numeric: tabular-nums;
}
.wf__big small { font-size: 0.9rem; color: var(--c-text-2, #a3a3a3); margin-left: 2px; }
.wf__name { display: flex; align-items: center; gap: 0.45rem; font-size: 0.8125rem; color: var(--c-text-2, #a3a3a3); }
.wf__name i { width: 10px; height: 10px; border-radius: 3px; }
@media (max-width: 560px) {
.wf { grid-template-columns: 1fr; }
.wf__legend { grid-template-columns: repeat(auto-fit, minmax(90px, 1fr)); }
}
@media (prefers-reduced-motion: reduce) { .wf__cell { animation: none; } }
</style>
Información Rápida
- Categoría
- Gráficos y Estadísticas
- Nombre de archivo
WaffleChart.astro- Dependencias
- apexcharts (peer)
- Etiquetas
- chartsvgzero-dependencywafflepercentagepart-to-wholegrid