Donut chart
A donut with a legend and an optional center label, over plain name and value data.
<script setup lang="ts">
import type { DonutSlice } from '@shared/components/common/charts/DonutChart.vue'
import DonutChart from '@shared/components/common/charts/DonutChart.vue'
const data: DonutSlice[] = [
{ name: 'Direct', value: 4820 },
{ name: 'Organic search', value: 3610 },
{ name: 'Referral', value: 1940 },
{ name: 'Email', value: 1180 },
{ name: 'Social', value: 760 },
]
</script>
<template>
<DonutChart :data="data" />
</template>Usage
data is DonutSlice[], which is { name, value, color? }. Slice colors come from the themed categorical palette unless you set one, so the chart follows the brand color and dark mode without being told.
It is a donut rather than a pie on purpose: the hole makes room for the center label and keeps the slices reading as a ring.
ECharts is heavy, so the template's dashboards import their charts with defineAsyncComponent. Do the same on a page where the chart is below the fold, and ECharts stays out of that page's first chunk.
The chart fills a wrapper of a definite height rather than being sized itself. vue-echarts ships .echarts { height: 100% }, which beats a height utility on the component and collapses the canvas.
Examples
A center label
centerValue and centerLabel print over the hole. They take no pointer events, so hovering the middle still reaches the slice underneath.
<script setup lang="ts">
import type { DonutSlice } from '@shared/components/common/charts/DonutChart.vue'
import DonutChart from '@shared/components/common/charts/DonutChart.vue'
import { computed } from 'vue'
const data: DonutSlice[] = [
{ name: 'Paid', value: 612 },
{ name: 'Pending', value: 148 },
{ name: 'Overdue', value: 63 },
]
const total = computed(() => data.reduce((sum, slice) => sum + slice.value, 0))
</script>
<template>
<DonutChart
:data="data"
:center-value="total.toLocaleString('en-US')"
center-label="Invoices"
/>
</template>Explicit colors
A slice with its own color keeps it. Use that where a color carries meaning, such as healthy, degraded and down.
<script setup lang="ts">
import type { DonutSlice } from '@shared/components/common/charts/DonutChart.vue'
import DonutChart from '@shared/components/common/charts/DonutChart.vue'
const data: DonutSlice[] = [
{ name: 'Healthy', value: 148, color: '#10b981' },
{ name: 'Degraded', value: 22, color: '#f59e0b' },
{ name: 'Down', value: 6, color: '#f43f5e' },
]
</script>
<template>
<DonutChart :data="data" :height="220" center-value="176" center-label="Services" />
</template>Loading
loading swaps the chart for a skeleton of the same height, so the card does not change size when the data arrives.
<script setup lang="ts">
import type { DonutSlice } from '@shared/components/common/charts/DonutChart.vue'
import DonutChart from '@shared/components/common/charts/DonutChart.vue'
import { Button } from '@shared/components/ui/button'
import { ref } from 'vue'
const data: DonutSlice[] = [
{ name: 'Desktop', value: 2400 },
{ name: 'Mobile', value: 3100 },
{ name: 'Tablet', value: 480 },
]
const loading = ref(true)
</script>
<template>
<div class="space-y-3">
<Button size="sm" variant="outline" @click="loading = !loading">
{{ loading ? 'Show the chart' : 'Show the skeleton' }}
</Button>
<DonutChart :data="data" :loading="loading" :height="220" />
</div>
</template>API
Source: src/shared/components/common/charts/DonutChart.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
data | DonutSlice[] | required | { name, value, color? }. |
loading | boolean | false | Renders a skeleton of the same height. |
height | number | 280 | The wrapper height in pixels. |
centerLabel | string | — | The small line in the hole. |
centerValue | string | number | — | The large line in the hole. |
Emits
None.
Slots
None.
Accessibility
- The tooltip is keyboard-inert, so the legend and the numbers beside the chart carry the values. Where a donut is the only source of a figure, print it as text too, which is what
centerValueis for. - Tooltip values are formatted in the app's language, read when the tooltip renders, so they follow the language switcher.
- Slice separators are drawn in the card's own surface color, so neighboring slices stay distinguishable in both color modes.