1 звезда на GitHub = 1 новый компонент.

Поставить звезду
Графики и статистика chartsvgzero-dependencywafflepercentagepart-to-wholegrid

Waffle Chart

Сетка 10×10, где каждая клетка — 1% целого; интуитивное отображение процентов.

Живое превью

Device share

Each square = 1% of sessions

  • 54% Desktop
  • 38% Mobile
  • 8% Tablet
View data
PartShare
Desktop54%
Mobile38%
Tablet8%

Использование

Скопируйте и вставьте этот код на вашу страницу 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 }, ]} />

Ручная установка

Если вы не используете пакет npm, создайте файл и вставьте следующий код: 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>

Краткая информация

Категория
Графики и статистика
Имя файла
WaffleChart.astro
Зависимости
apexcharts (peer)
Теги
chartsvgzero-dependencywafflepercentagepart-to-wholegrid