diff --git a/@kiva/kv-components/SKILLS.md b/@kiva/kv-components/SKILLS.md new file mode 100644 index 00000000..ec3caa8c --- /dev/null +++ b/@kiva/kv-components/SKILLS.md @@ -0,0 +1,31 @@ +# @kiva/kv-components Skills Catalog + +AI-assisted development guides for common workflows in this package. + +## Available Skills + +| Skill | File | Description | +|-------|------|-------------| +| make-to-vue | [docs/make-to-vue.md](docs/make-to-vue.md) | Use when porting a Figma Make (or other React) component into a Vue 3 kv-components component. Triggered by requests like "port this to Vue" or by the user sharing React source, a Figma Make URL, or a Figma file link. | + +## AI Prompts + +Drop-in prompts for generating Storybook stories and MDX documentation with an AI assistant. + +| Prompt | File | Description | +|--------|------|-------------| +| ai-stories-prompt | [docs/ai-stories-prompt.md](docs/ai-stories-prompt.md) | Prompt for generating standardized Storybook `.stories.js` files for a component. | +| ai-documentation-prompt | [docs/ai-documentation-prompt.md](docs/ai-documentation-prompt.md) | Prompt for generating the matching `Docs.mdx` documentation page for a component. | +| how-to-use-ai-prompts | [docs/how-to-use-ai-prompts.md](docs/how-to-use-ai-prompts.md) | Walkthrough of when and how to use the prompt files above. | + +## Authoring Guides & Checklists + +Reference material the AI prompts pull from — useful when reviewing output by hand or authoring without a prompt. + +| Doc | File | Description | +|-----|------|-------------| +| Component Stories Guide | [docs/component-stories-guide.md](docs/component-stories-guide.md) | Standards and best practices for Storybook stories in kv-components. | +| Component Stories Checklist | [docs/component-stories-checklist.md](docs/component-stories-checklist.md) | Checklist for verifying story coverage and quality before merging. | +| Component Documentation Guide | [docs/component-documentation-guide.md](docs/component-documentation-guide.md) | Standards and best practices for MDX component documentation. | +| Component Documentation Checklist | [docs/component-documentation-checklist.md](docs/component-documentation-checklist.md) | Checklist for verifying documentation completeness before merging. | +| Storybook Folder Prefixes | [docs/storybook-folder-prefixes.md](docs/storybook-folder-prefixes.md) | Required title prefixes for organizing components in the Storybook sidebar. | diff --git a/@kiva/kv-components/docs/make-to-vue.md b/@kiva/kv-components/docs/make-to-vue.md new file mode 100644 index 00000000..84579801 --- /dev/null +++ b/@kiva/kv-components/docs/make-to-vue.md @@ -0,0 +1,264 @@ +--- +name: make-to-vue +description: Use when porting a Figma Make (or other React) component into a Vue 3 component for @kiva/kv-components — typically triggered by a request like "port this to Vue", "convert this React component", or when the user shares Figma Make code, a Figma Make URL, or a Figma file link. +--- + +# Figma Make to Vue Component Porting Guide + +A repeatable process for converting Figma Make-generated React components into Vue 3 components for `@kiva/kv-components`. + +## How to Use This Skill + +**Announce at start:** "I'm using the make-to-vue skill to port `` into kv-components." + +Then walk the [Procedure](#procedure) below in order. Each phase has explicit verification steps; do not move to the next phase until the current one passes. If any **Stop-and-ask** condition fires, pause and check with the user before continuing. + +### Inputs You Need Before Starting + +Before Phase 1, confirm you have at least one of the following. If none are present, **stop and ask the user** which they can provide: + +- React/TSX source code for the component (pasted, attached, or saved into the repo — `/source-files/` at the repo root is a suggested location but not required) +- A Figma Make URL (so the source can be inspected/exported) +- A Figma file or frame link for the target design (for visual reference, color, and spacing verification) + +Screenshots help but are not a substitute for source code or a Figma link. + +### Stop-and-Ask Conditions + +Pause and ask the user rather than guessing when: + +- No source code or Figma Make URL is available — porting from a screenshot alone will produce drift. +- The component overlaps an existing kv-components component (see Phase 1.2). Confirm whether to extend the existing one, create a `V2`, or build a new component. +- Hardcoded colors don't map cleanly to a design token. Confirm the intended token rather than inventing a new hex. +- The source uses an animation library (`motion/react`, `framer-motion`, etc.) and the equivalent CSS transition would visibly differ. Confirm whether to match exactly or adapt. +- Accessibility behavior is unclear (e.g., what should screen readers announce, what should keyboard interactions trigger). + +## Procedure + +The [Quick Reference: Porting Checklist](#quick-reference-porting-checklist) at the end of this doc is the spine — work through it top-to-bottom. The phases below elaborate each step. After every phase, run the listed verification before moving on. + +### Verification Gates + +| After Phase | Verify | +|-------------|--------| +| 1 (Analysis) | You can name the props, state, sub-components, animations, and existing equivalents. If not, re-read the source. | +| 2 (Translation) | Component renders in Storybook with representative data and no console errors. | +| 3 (Structure) | Component is exported in `src/vue/index.ts` and visible in Storybook sidebar under the correct folder prefix. | +| 4 (Design system) | No raw hex colors or pixel spacing remain unless the user explicitly approved them. | +| 5 (Testing) | `npm run test` passes (lint + jest), including `jest-axe` accessibility check. | +| 6 (Storybook) | Default + edge-case stories render correctly under visual review. | + +If a verification fails, fix the underlying issue before continuing — do not paper over it to keep moving. + +## Prerequisites + +- Source React code available (see [Inputs You Need Before Starting](#inputs-you-need-before-starting)) +- Familiarity with the target component's Figma design (request links/screenshots if needed) +- Read `@kiva/kv-components/AGENTS.md` for component development patterns + +## Phase 1: Analysis + +### 1.1 Read the React Source + +Read the source file and identify: + +- **Props and data model** — What data does the component accept? Is it hardcoded or dynamic? +- **State management** — React hooks (`useState`, `useEffect`, `useRef`) that need Vue equivalents +- **Sub-components** — Internal components that may become part of the Vue file or separate utils +- **External dependencies** — Libraries used (e.g., `motion/react`, `framer-motion`) +- **Styling approach** — Tailwind classes, inline styles, CSS modules + +### 1.2 Check for Existing Equivalents + +``` +# Check if a similar component already exists +Glob: @kiva/kv-components/src/vue/Kv**.vue + +# Check for reusable utils +Glob: @kiva/kv-components/src/utils/**/* +``` + +Decide: new component, v2 of existing, or enhancement to existing. + +### 1.3 Identify What Changes + +Figma Make output is presentational/demo code. Determine what needs to change: + +- **Hardcoded data** -> Dynamic props +- **Hardcoded colors** -> Design system tokens from `@kiva/kv-tokens/primitives.js` +- **Fixed dimensions** -> Container-responsive sizing +- **Presentational strings** -> Configurable via props/slots +- **React animation libraries** -> CSS transitions or Vue composables (prefer no extra deps) + +## Phase 2: Translation Patterns + +### 2.1 React Hooks to Vue Composition API + +| React | Vue 3 | +|-------|-------| +| `useState(value)` | `ref(value)` | +| `useEffect(() => {}, [deps])` | `watch(deps, () => {})` or `onMounted(() => {})` | +| `useRef(null)` | `ref(null)` or `templateRef` | +| `useMemo(() => {}, [deps])` | `computed(() => {})` | +| `useCallback(() => {}, [deps])` | Plain function (no memoization needed in Vue) | +| Custom hooks | Composables in `src/utils/use*.ts` | + +### 2.2 JSX to Vue Template + +| React JSX | Vue Template | +|-----------|-------------| +| `{condition &&
}` | `
` | +| `{items.map(item => )}` | `` | +| `className="..."` | `class="..."` | +| `style={{ color: 'red' }}` | `:style="{ color: 'red' }"` | +| `onClick={handler}` | `@click="handler"` | +| `{children}` | `` | +| `` | CSS transitions or Vue transition components | + +### 2.3 Styling Translation + +Figma Make outputs raw Tailwind classes. Convert to kv-components patterns: + +- **Add `tw-` prefix** to all Tailwind utility classes +- **Replace hardcoded font families** with design system fonts (`tw-font-sans`, `tw-font-serif`) +- **Replace hardcoded colors** with token references where possible +- **Replace pixel values** in Tailwind classes with spacing scale values (8px increments) +- **Use design tokens** from `@kiva/kv-tokens/primitives.js` for colors, spacing, typography + +### 2.4 Animation Translation + +Prefer CSS transitions over JavaScript animation libraries: + +| Figma Make / React | Vue Equivalent | +|--------------------|----------------| +| `motion/react` animate prop | CSS `transition` property | +| `transition={{ duration: 0.5 }}` | `transition: property 500ms ease-in-out` | +| Staggered animations | Computed `transition-delay` per item index | +| Spring animations | CSS `cubic-bezier()` or `ease-in-out` | +| `requestAnimationFrame` hooks | Vue composable with `onMounted`/`onUnmounted` lifecycle | +| Opacity/transform tweens | CSS transitions on those properties | + +For complex numeric animations (count-up, interpolation), create a composable in `src/utils/`: + +```ts +// src/utils/useCountUp.ts +import { ref, watch, onUnmounted } from 'vue'; + +export function useCountUp(target: Ref, active: Ref, duration = 500) { + // requestAnimationFrame-based tween with easing + // Returns reactive ref with current display value +} +``` + +## Phase 3: Component Structure + +### 3.1 File Creation Checklist + +1. `src/vue/Kv.vue` — Main component +2. `src/utils/.ts` — Extracted logic (composables, helpers) +3. `src/vue/stories/Kv.stories.ts` — Storybook stories (create EARLY) +4. `tests/unit/specs/components/Kv.spec.ts` — Component tests +5. `tests/unit/specs/utils/.spec.ts` — Utility tests +6. Export in `src/vue/index.ts` + +### 3.2 Component Patterns + +Follow these kv-components conventions: + +- **Script**: ` + + diff --git a/@kiva/kv-components/src/vue/index.ts b/@kiva/kv-components/src/vue/index.ts index 757a7744..5fd28159 100644 --- a/@kiva/kv-components/src/vue/index.ts +++ b/@kiva/kv-components/src/vue/index.ts @@ -58,6 +58,7 @@ export { default as KvMaterialIcon } from './KvMaterialIcon.vue'; export { default as KvPageContainer } from './KvPageContainer.vue'; export { default as KvPagination } from './KvPagination.vue'; export { default as KvPieChart } from './KvPieChart.vue'; +export { default as KvPieChartV2 } from './KvPieChartV2.vue'; export { default as KvPill } from './KvPill.vue'; export { default as KvPopper } from './KvPopper.vue'; export { default as KvProgressBar } from './KvProgressBar.vue'; diff --git a/@kiva/kv-components/src/vue/stories/KvPieChartV2.stories.js b/@kiva/kv-components/src/vue/stories/KvPieChartV2.stories.js new file mode 100644 index 00000000..adb326cb --- /dev/null +++ b/@kiva/kv-components/src/vue/stories/KvPieChartV2.stories.js @@ -0,0 +1,475 @@ +import KvPieChartV2 from '../KvPieChartV2.vue'; +import KvPieChartV2DocsMdx from './KvPieChartV2Docs.mdx'; + +const sampleValues = [ + { label: 'Agriculture', value: 28 }, + { label: 'Eco-friendly', value: 28 }, + { label: 'Services', value: 17 }, + { label: 'Water', value: 13 }, + { label: 'Food', value: 12 }, + { label: 'Other', value: 5 }, +]; + +const fewValues = [ + { label: 'Female', value: 75 }, + { label: 'Male', value: 25 }, +]; + +const manyValues = [ + { label: 'Food', value: 575 }, + { label: 'Retail', value: 377 }, + { label: 'Agriculture', value: 285 }, + { label: 'Services', value: 211 }, + { label: 'Clothing', value: 183 }, + { label: 'Arts', value: 65 }, + { label: 'Housing', value: 65 }, + { label: 'Education', value: 36 }, + { label: 'Construction', value: 28 }, + { label: 'Health', value: 27 }, + { label: 'Transportation', value: 23 }, + { label: 'Personal Use', value: 19 }, + { label: 'Manufacturing', value: 13 }, + { label: 'Entertainment', value: 10 }, + { label: 'Wholesale', value: 5 }, +]; + +const customColorValues = [ + { label: 'Category A', value: 40, color: '#FF6B6B' }, + { label: 'Category B', value: 30, color: '#4ECDC4' }, + { label: 'Category C', value: 20 }, + { label: 'Category D', value: 10 }, +]; + +export default { + title: 'Charts/KvPieChartV2', + component: KvPieChartV2, + parameters: { + docs: { + page: KvPieChartV2DocsMdx, + title: 'KvPieChartV2 Docs', + }, + }, + argTypes: { + /** + * Chart data items. Each must have a label and numeric value; optional color override. + */ + values: { + control: 'object', + description: 'Chart data items. Each must have a label and numeric value; optional color override.', + table: { + type: { summary: 'KvPieChartV2Item[]' }, + defaultValue: { summary: '[]' }, + }, + }, + /** + * How values are displayed in legend pills. + */ + unit: { + control: { type: 'radio' }, + options: ['percent', 'amount', 'count'], + description: 'How values are displayed in legend pills.', + table: { + type: { summary: "'percent' | 'amount' | 'count'" }, + defaultValue: { summary: "'percent'" }, + }, + }, + /** + * Shows a skeleton donut ring when true. + */ + loading: { + control: 'boolean', + description: 'Shows a skeleton donut ring when true.', + table: { + type: { summary: 'boolean' }, + defaultValue: { summary: 'false' }, + }, + }, + /** + * Maximum number of visible segments before remaining items collapse into "Other". + */ + shownSegments: { + control: { type: 'number', min: 1, max: 20 }, + description: 'Maximum number of visible segments before remaining items collapse into "Other".', + table: { + type: { summary: 'number' }, + defaultValue: { summary: '5' }, + }, + }, + /** + * Donut ring thickness in SVG user units. The radius shrinks as this grows, + * so the outer edge always fits within the viewBox. + */ + strokeWidth: { + control: { + type: 'range', min: 8, max: 200, step: 2, + }, + description: 'Donut ring thickness in SVG user units.', + table: { + type: { summary: 'number' }, + defaultValue: { summary: '56' }, + }, + }, + /** + * Visual gap between adjacent segments, in SVG user units along the circumference. + */ + segmentGap: { + control: { + type: 'range', min: 0, max: 20, step: 1, + }, + description: 'Visual gap between adjacent segments, in SVG user units along the circumference.', + table: { + type: { summary: 'number' }, + defaultValue: { summary: '2' }, + }, + }, + /** + * Delay in milliseconds before the first segment starts animating in. + */ + initialDelay: { + control: { + type: 'range', min: 0, max: 5000, step: 100, + }, + description: 'Delay in milliseconds before the first segment starts animating in.', + table: { + type: { summary: 'number' }, + defaultValue: { summary: '1000' }, + }, + }, + /** + * Reverses the entrance animation (segments shrink back to 0). + */ + animateOut: { + control: 'boolean', + description: 'Reverses the entrance animation (segments shrink back to 0).', + table: { + type: { summary: 'boolean' }, + defaultValue: { summary: 'false' }, + }, + }, + }, +}; + +/** + * Default story - Interactive playground for exploring all props. + */ +export const Default = { + args: { + values: sampleValues, + unit: 'percent', + loading: false, + shownSegments: 5, + strokeWidth: 56, + segmentGap: 2, + initialDelay: 1000, + animateOut: false, + }, + render: (args) => ({ + components: { KvPieChartV2 }, + setup() { + return { args }; + }, + template: ` +
+
+ +
+
+ `, + }), +}; + +/** + * Component Overview - Key visual variants at a glance. + */ +export const ComponentOverview = { + render: () => ({ + components: { KvPieChartV2 }, + setup() { + return { sampleValues, fewValues, manyValues }; + }, + template: ` +
+
+ +

Default (6 items)

+
+
+ +

Few items

+
+
+ +

Many items (Other pill)

+
+
+ +

Loading skeleton

+
+
+ `, + }), +}; + +/** + * All Variations - Every prop axis shown in organized groups. + */ +export const AllVariations = { + render: () => ({ + components: { KvPieChartV2 }, + setup() { + return { + sampleValues, manyValues, customColorValues, + }; + }, + template: ` +
+
+

Unit Formats

+
+
+ +

percent

+
+
+ +

amount

+
+
+ +

count

+
+
+
+ +
+

Ring Thickness

+
+
+ +

strokeWidth: 24

+
+
+ +

strokeWidth: 56 (default)

+
+
+ +

strokeWidth: 100

+
+
+
+ +
+

Segment Gap

+
+
+ +

segmentGap: 0

+
+
+ +

segmentGap: 2 (default)

+
+
+ +

segmentGap: 10

+
+
+
+ +
+

Data Density

+
+
+ +

Single value

+
+
+ +

Many values, shownSegments: 4

+
+
+ +

Custom color overrides

+
+
+
+
+ `, + }), +}; + +/** + * Loading State - Skeleton ring while data is unavailable. + */ +export const LoadingState = { + render: () => ({ + components: { KvPieChartV2 }, + template: ` +
+
+ +
+
+ `, + }), +}; + +/** + * Unit Formats - Side-by-side comparison of the three display modes. + */ +export const UnitFormats = { + render: () => ({ + components: { KvPieChartV2 }, + setup() { + return { sampleValues }; + }, + template: ` +
+
+ +

percent

+
+
+ +

amount

+
+
+ +

count

+
+
+ `, + }), +}; + +/** + * Ring Thickness - strokeWidth scales inward so outer diameter stays constant. + */ +export const RingThickness = { + render: () => ({ + components: { KvPieChartV2 }, + setup() { + return { sampleValues }; + }, + template: ` +
+
+ +

16 (thin)

+
+
+ +

56 (default)

+
+
+ +

120 (thick)

+
+
+ `, + }), +}; + +/** + * Segment Gap - Visual spacing between adjacent segments. + */ +export const SegmentGap = { + render: () => ({ + components: { KvPieChartV2 }, + setup() { + return { sampleValues }; + }, + template: ` +
+
+ +

No gap

+
+
+ +

2 (default)

+
+
+ +

10 (wide)

+
+
+ `, + }), +}; + +/** + * Data Density - Single value, few items, and overflow into "Other". + */ +export const DataDensity = { + render: () => ({ + components: { KvPieChartV2 }, + setup() { + return { fewValues, manyValues }; + }, + template: ` +
+
+ +

Single value

+
+
+ +

Few items

+
+
+ +

Many items (click Other)

+
+
+ `, + }), +}; + +/** + * Custom Colors - Partial or full color overrides per item. + */ +export const CustomColors = { + render: () => ({ + components: { KvPieChartV2 }, + setup() { + return { customColorValues }; + }, + template: ` +
+
+ +
+
+ `, + }), +}; + +/** + * Animation Timing - Initial delay options including no delay. + */ +export const AnimationTiming = { + render: () => ({ + components: { KvPieChartV2 }, + setup() { + return { sampleValues }; + }, + template: ` +
+
+ +

initialDelay: 0

+
+
+ +

initialDelay: 1000 (default)

+
+
+ +

initialDelay: 2500

+
+
+ `, + }), +}; diff --git a/@kiva/kv-components/src/vue/stories/KvPieChartV2Docs.mdx b/@kiva/kv-components/src/vue/stories/KvPieChartV2Docs.mdx new file mode 100644 index 00000000..e1389eba --- /dev/null +++ b/@kiva/kv-components/src/vue/stories/KvPieChartV2Docs.mdx @@ -0,0 +1,272 @@ +import { Canvas, Meta, Story, Controls } from '@storybook/addon-docs/blocks'; +import * as KvPieChartV2Stories from './KvPieChartV2.stories.js'; + + + +# KvPieChartV2 + +An animated donut chart with an accompanying legend, designed for compact displays of categorical breakdowns with optional overflow into an "Other" lightbox. + +## Component Overview + +KvPieChartV2 renders a sorted donut chart with one ring segment per data item and a two-column legend of color-coded pills. Segments animate in with a staggered grow effect while their values count up from zero. When the dataset exceeds `shownSegments`, the remaining items collapse into an "Other" segment whose pill opens a lightbox listing every item in descending order. + + + +## Table of Contents + +- [Variations](#variations) +- [Usage Information](#usage-information) +- [Behavior](#behavior) +- [Anatomy](#anatomy) +- [Specs](#specs) +- [Best Practices](#best-practices) +- [Accessibility](#accessibility) +- [Component Properties + Demo](#component-properties) +- [Code Examples](#code-examples) + +--- + +## Variations + +The component exposes several axes of customization: + +- **Unit Format**: Display legend values as `percent`, `amount` (with `$` prefix), or raw `count` +- **Ring Thickness**: `strokeWidth` controls how thick the donut ring is; the radius shrinks as it grows so the outer edge always fits the viewBox +- **Segment Gap**: `segmentGap` adds visual breathing room between adjacent segments +- **Segment Cap**: `shownSegments` caps the number of distinct segments before the rest collapse into "Other" +- **Custom Colors**: Each item can override its assigned palette color via an optional `color` field +- **Animation Timing**: `initialDelay` postpones the entrance animation; `animateOut` reverses it + + + +## Usage Information + +KvPieChartV2 is intended for compact dashboard tiles, profile cards, and modal panels where a categorical breakdown should feel approachable rather than data-dense. The animated entrance draws attention to the chart on first view, and the "Other" overflow keeps the legend scannable even when the underlying dataset is long. + +### When to Use + +- To show how a small number of categories add up to a whole (sectors, gender, status, etc.) +- In compact contexts (~262px wide) where a full chart library would be overkill +- When animated reveals add value to the experience (e.g., on loan or impact pages) +- When you want a clickable "Other" affordance for long-tail data + +### When Not to Use + +- For precise, comparison-heavy data — use a bar chart or table instead +- For time-series data — use [KvLineGraph](/?path=/docs/charts-kvlinegraph--docs) +- When the dataset has dozens of equally-weighted categories that resist meaningful collapse +- For interactive drill-down or hover tooltips — the component is read-only beyond the "Other" lightbox + +--- + +## Behavior + +The chart's behavior centers on its entrance animation, the "Other" overflow affordance, and the loading skeleton. + +### Loading State + +When `loading` is true, the chart renders a neutral gray skeleton ring in place of the segments and hides the legend. This keeps layout stable while data fetches and avoids partial renders. As soon as `loading` flips to false (with `values` present), the entrance animation begins after `initialDelay`. + + + +### Other Segment Overflow + +When the number of items exceeds `shownSegments`, all overflow items are merged into a single gray "Other" segment in the ring and a corresponding "Other" pill in the legend. Clicking the pill opens a lightbox showing **every** item — both visible and collapsed — sorted in descending order with their assigned colors. This lets consumers scan the long tail without inflating the chart itself. + + + +## Anatomy + +The component is composed of: + +- **Donut ring**: An SVG with one `` per visible segment and an optional gray "Other" circle, all rendered with `stroke-dasharray` to control segment length +- **Skeleton ring**: A neutral gray placeholder circle shown while `loading` is true or when no data is present +- **Legend grid**: A two-column grid of color-coded pills, each showing the segment label and its animated value +- **Other pill**: A trigger pill that opens the lightbox when overflow exists +- **Other lightbox**: A modal listing all items in descending order, rendered via [KvLightbox](/?path=/docs/components-kvlightbox--docs) + +--- + +## Specs + +### Ring Thickness + +The donut renders inside a fixed `262 × 262` viewBox. The radius is derived from `strokeWidth` so the outer edge always lands on the viewBox boundary — thicker rings grow inward toward center rather than overflowing the SVG. Common values: + +- **16-24**: Thin, subtle rings +- **56** (default): Balanced standard appearance +- **80-120**: Bold, prominent rings (smaller inner hole) + + + +### Segment Gap + +The `segmentGap` prop subtracts visual length (in SVG user units along the circumference) from each segment so adjacent segments appear separated. A value of `0` produces a continuous ring; the default of `2` matches the design spec; values around `8-10` create distinct "petal" segments. + +### Animation Timing + +The entrance animation runs in three coordinated phases: + +- **Initial delay** (`initialDelay`, default `1000ms`): Wait before any segment animates +- **Per-segment grow** (500ms): Each segment expands clockwise from its starting angle +- **Stagger** (500ms): Each segment begins as the previous one completes + +Count-up animations on legend pill values run in parallel with their corresponding segment animations using cubic ease-in-out. + +--- + +## Best Practices + +
+
+
+ +
+
✓ Do
+

Cap visible segments at 5–6 with `shownSegments` and let the rest collapse into "Other" so the legend stays scannable.

+
+ +
+
+ +
+
✗ Don't
+

Render every category as its own segment. Crowded rings make individual slices unreadable and force the legend to grow indefinitely.

+
+ +
+
+ +
+
✓ Do
+

Pick the `unit` that matches the underlying data: `percent` for share-of-whole, `amount` for currency, `count` for raw tallies.

+
+ +
+
+ +
+
✗ Don't
+

Display amounts as percents (or vice versa) just because it looks tidier. The unit must reflect what the chart actually measures.

+
+
+ +--- + +## Accessibility + +- The chart `
` carries an `aria-label="Pie chart"` so it is announced as an image to assistive tech +- The decorative SVG is marked `aria-hidden="true"` since the legend pills carry the same data in textual form +- The "Other" pill is a real `