Donut Chart

Build single-container donut and radial summaries.
alpha

Donut charts reserve the center for a total or short summary. Place VisDonut inside VisSingleContainer; ChartRoot still supplies palette variables, tooltip configuration, and legend metadata.

Summary

Use the central label for a concise total and keep the surrounding legend visible. Segment tooltips can use ChartTooltip with :targets="[Donut]".

Direct
Search
Referral
vue
<script setup lang="ts">
import { Donut } from '@unovis/ts'
import { VisDonut, VisSingleContainer } from '@unovis/vue'
import {
  ChartLegend,
  ChartRoot,
  ChartTooltip,
} from '@typlog/ui/charts'

const data = [
  { label: 'Direct', value: 46 },
  { label: 'Search', value: 31 },
  { label: 'Referral', value: 23 },
]
const config = Object.fromEntries(data.map(item => [
  item.label,
  { label: item.label },
]))
</script>

<template>
  <ChartRoot v-slot="{ mounted }" :config="config">
    <VisSingleContainer v-if="mounted" :data="data" :height="220">
      <VisDonut
        :value="(item: { value: number }) => item.value"
        central-label="100%"
      />
      <ChartTooltip :targets="[Donut]" :content-props="{ hideLabel: true }" />
    </VisSingleContainer>
    <ChartLegend class="mt-3" />
  </ChartRoot>
</template>

Thin ring

A thin ring gives more visual weight to the central number. Rounded corners and slight padding work best with a small number of segments.

Free
Pro
Business
vue
<script setup lang="ts">
import { VisDonut, VisSingleContainer } from '@unovis/vue'
import { ChartLegend, ChartRoot } from '@typlog/ui/charts'

interface Slice {
  plan: string
  accounts: number
}

const data: Slice[] = [
  { plan: 'Free', accounts: 62 },
  { plan: 'Pro', accounts: 28 },
  { plan: 'Business', accounts: 10 },
]
const config = Object.fromEntries(data.map(item => [item.plan, { label: item.plan }]))
</script>

<template>
  <ChartRoot v-slot="{ mounted }" :config="config">
    <div class="mx-auto max-w-72">
      <VisSingleContainer v-if="mounted" :data="data" :height="240">
        <VisDonut
          :value="(slice: Slice) => slice.accounts"
          :arc-width="12"
          :pad-angle="0.025"
          :corner-radius="6"
          central-label="100"
          central-sub-label="Accounts"
        />
      </VisSingleContainer>
    </div>
    <ChartLegend class="mt-3" align="center" />
  </ChartRoot>
</template>

Progress gauge

A half donut can present progress against a fixed total. Include both used and available segments so the geometry still represents the complete denominator.

Used
Available
vue
<script setup lang="ts">
import { VisDonut, VisSingleContainer } from '@unovis/vue'
import { ChartLegend, ChartRoot } from '@typlog/ui/charts'

interface Slice {
  status: string
  value: number
}

const data: Slice[] = [
  { status: 'Used', value: 72 },
  { status: 'Available', value: 28 },
]
const config = {
  Used: { label: 'Used', color: 'var(--green-9)' },
  Available: { label: 'Available', color: 'var(--gray-6)' },
}
</script>

<template>
  <ChartRoot v-slot="{ mounted }" :config="config">
    <div class="mx-auto max-w-72">
      <VisSingleContainer v-if="mounted" :data="data" :height="190">
        <VisDonut
          :value="(slice: Slice) => slice.value"
          :arc-width="22"
          :angle-range="[-Math.PI / 2, Math.PI / 2]"
          :background-angle-range="[-Math.PI / 2, Math.PI / 2]"
          :corner-radius="8"
          central-label="72%"
          central-sub-label="Capacity used"
          :central-label-offset-y="24"
        />
      </VisSingleContainer>
    </div>
    <ChartLegend class="mt-3" align="center" />
  </ChartRoot>
</template>

Do not use the central label for a second, unrelated metric. It should summarize the ring or provide the total represented by its segments.

Last updated Sep 6, 2026