Prisma
Live Preview
Weekly Revenue
Bar chart — category comparison
Mon
Tue
Wed
Thu
Fri
Sat
Sun
Payment Methods
Donut chart — distribution
Cash — 55%
QR — 30%
Transfer — 15%
Trend
Line chart — over time
JanFebMarAprMayJunJul

Chart Component Generation Skill

Skill này hướng dẫn bạn (AI Agent) tạo component Chart — data visualization với Bar, Line, Pie/Donut charts, dùng Chart.js + Prisma Token theming.

1. Mục tiêu (Objective)

Tạo Chart wrapper component tích hợp Chart.js, dùng Prisma --chart-* CSS variables cho theming, auto dark mode, wrapped trong Card container.

2. AI Context & Intent (Ngữ cảnh cho AI)

Khi nào dùng Chart?

  • Revenue report: Bar chart doanh thu theo ngày/tuần/tháng
  • Dashboard: KPI trends, distribution pie charts
  • Analytics: Line chart tracking over time

Decision Tree cho AI

text
Cần visualize dữ liệu?
├─ So sánh categories → Bar Chart
├─ Trend over time → Line Chart
├─ Tỷ lệ phân bố → Pie / Donut Chart
├─ Data table → Table component
└─ Single number → KPI Card

3. Implementation Strategy

3.1. Library Choice: Chart.js

Criteria Chart.js Recharts D3
Framework-agnostic ❌ (React only)
Bundle size ~60KB ~120KB ~240KB
Canvas (perf) ❌ (SVG) Either
Built-in types ❌ (build from scratch)
Winner

3.2. CSS Variable Bridge (shadcn approach)

Chart.js doesn't natively read CSS variables. Bridge via JS:

javascript
// Read Prisma CSS variables for Chart.js config
function getChartColor(varName) {
  return getComputedStyle(document.documentElement)
    .getPropertyValue(varName).trim();
}

const chartColors = {
  a: getChartColor('--chart-class-a'),
  b: getChartColor('--chart-class-b'),
  c: getChartColor('--chart-class-c'),
  d: getChartColor('--chart-class-d'),
  e: getChartColor('--chart-class-e'),
  f: getChartColor('--chart-class-f'),
  grid: getChartColor('--border'),
  text: getChartColor('--muted-foreground'),
  tooltip: {
    bg: getChartColor('--surface'),
    fg: getChartColor('--surface-foreground'),
    border: getChartColor('--border'),
  }
};

3.3. Dark Mode

Re-read CSS variables when data-theme changes:

javascript
// Observe theme changes
const observer = new MutationObserver(() => {
  const newColors = readChartColors();
  chart.options.scales.x.ticks.color = newColors.text;
  chart.options.scales.y.ticks.color = newColors.text;
  chart.options.scales.x.grid.color = newColors.grid;
  chart.data.datasets.forEach((ds, i) => {
    ds.backgroundColor = newColors[Object.keys(newColors)[i]];
  });
  chart.update();
});
observer.observe(document.documentElement, {
  attributes: true, attributeFilter: ['data-theme']
});

4. Token Mapping

📦 Atomic Mapping: Xem ATOMIC-MAPPING.md → mục chart — UI Layer: Card, Density Tier: section.

Property Token Ghi chú
Container card component tokens Wrapped in card
Grid lines --border
Axis tick labels --muted-foreground caption text style
Tooltip bg --surface
Tooltip fg --surface-foreground
Tooltip border --border
Tooltip radius --comp-radius
Data color 1 --chart-class-a Primary data
Data color 2 --chart-class-b Secondary data
Data color 3 --chart-class-c Tertiary data
Data color 4 --chart-class-d
Data color 5 --chart-class-e
Data color 6 --chart-class-f
Legend text body-small, --foreground
Responsive maintainAspectRatio: true

5. Chart Types

5.1. Bar Chart

javascript
// xPOS Revenue Report example
new Chart(ctx, {
  type: 'bar',
  data: {
    labels: ['T2', 'T3', 'T4', 'T5', 'T6', 'T7', 'CN'],
    datasets: [{
      label: 'Doanh thu',
      data: [1200000, 1900000, 800000, 1500000, 2100000, 1700000, 900000],
      backgroundColor: getChartColor('--chart-class-a'),
      borderRadius: parseInt(getChartColor('--comp-radius')) || 4,
    }]
  },
  options: {
    responsive: true,
    maintainAspectRatio: true,
    plugins: { legend: { display: false } },
    scales: {
      x: { grid: { display: false }, ticks: { color: chartColors.text } },
      y: { grid: { color: chartColors.grid }, ticks: { color: chartColors.text } }
    }
  }
});

5.2. Line Chart

javascript
new Chart(ctx, {
  type: 'line',
  data: {
    labels: months,
    datasets: [{
      label: 'Doanh thu',
      data: values,
      borderColor: getChartColor('--chart-class-a'),
      backgroundColor: getChartColor('--chart-class-a') + '20', // 12% opacity
      fill: true,
      tension: 0.3,
      pointRadius: 4,
    }]
  }
});

5.3. Pie / Donut Chart

javascript
new Chart(ctx, {
  type: 'doughnut',
  data: {
    labels: ['Tiền mặt', 'QR', 'Chuyển khoản'],
    datasets: [{
      data: [55, 30, 15],
      backgroundColor: [
        getChartColor('--chart-class-a'),
        getChartColor('--chart-class-b'),
        getChartColor('--chart-class-c'),
      ],
      borderWidth: 0,
    }]
  },
  options: {
    cutout: '60%', // Donut hole
    plugins: { legend: { position: 'bottom' } }
  }
});

6. Accessibility (a11y)

  • Canvas fallback: Provide data table as <table> inside <noscript> or hidden accessible summary
  • ARIA: role="img" + aria-label describing chart data on canvas element
  • Colors: Chart colors must have sufficient contrast; avoid relying on color alone — use patterns/labels
  • Focus: Legend items should be focusable for keyboard navigation
  • Reduced motion: Disable Chart.js animations when prefers-reduced-motion

7. Best Practices & Rules

  • Không Hardcode colors: Always read from CSS variables via bridge function
  • Card wrapper: Always wrap charts in Prisma card component
  • Responsive: Use Chart.js responsive: true + maintainAspectRatio: true
  • Legend: Bottom position for horizontal charts, right for vertical
  • Tooltips: Use custom HTML tooltip styled with Prisma tokens
  • Loading: Show skeleton component while chart data loads
  • Empty state: Show empty-state component when no data

8. Example Usage

html
<!-- Bar chart wrapped in card -->
<div class="card">
  <div class="slot-header">
    <h3>Doanh thu tuần này</h3>
  </div>
  <div class="slot-body">
    <canvas id="revenue-chart"></canvas>
  </div>
</div>

<!-- Donut chart -->
<div class="card">
  <div class="slot-header">
    <h3>Phương thức thanh toán</h3>
  </div>
  <div class="slot-body">
    <canvas id="payment-chart"></canvas>
  </div>
</div>