Data Visualisation
Donut chart
<DomDonutChart>Compare non-negative category shares with a centre total, selectable legend, and accessible data table.
Category shares and selection
September 2026 · Unique visitors
View data for Where visitors come from
| Category | Value |
|---|---|
| 8,240 | |
| 3,620 | |
| 2,180 | |
| 1,460 |
npm install @getdom/studio<script setup>
import '@getdom/studio/style.css';
import { ref } from 'vue';
import { DomDonutChart } from '@getdom/studio';
const selected = ref(null);
const rows = [{ channel: 'Organic search', visitors: 8240, color: 'chart-1' }, { channel: 'Direct', visitors: 3620, color: 'chart-2' }, { channel: 'Referral', visitors: 2180, color: 'chart-3' }, { channel: 'Email', visitors: 1460, color: 'chart-4' }];
</script>
<template>
<div class="w-full">
<DomDonutChart title="Where visitors come from" description="September 2026 · Unique visitors" :rows="rows" label-key="channel" value-key="visitors" total-label="Visitors" @select="selected = $event.row" />
<p v-if="selected" class="mt-3 text-xs text-muted-fg">Selected channel: {{ selected.channel }}. Your application can open its detailed report.</p>
</div>
</template>
Overall technical SEO score
A score out of 100 with a thin ring and a matching centre label. Adjust the example score to preview the rating bands.
Overall audit · Example report
Good
Strong technical foundations
Keep resolving the remaining audit findings and monitor changes as the site grows.
npm install @getdom/studio<script setup>
import '@getdom/studio/style.css';
import { computed, ref } from 'vue';
import { DomDonutChart } from '@getdom/studio';
const score = ref(86);
const rating = computed(getRating);
const rows = computed(getRows);
/** @returns {object} Application-defined rating for the current example score. */
function getRating() {
if (score.value >= 80) return { label: 'Good', color: 'success', heading: 'Strong technical foundations', description: 'Keep resolving the remaining audit findings and monitor changes as the site grows.' };
if (score.value >= 50) return { label: 'Needs improvement', color: 'warning', heading: 'Room for improvement', description: 'Review the highest-impact findings first, then work through the remaining technical issues.' };
return { label: 'Poor', color: 'destructive', heading: 'Technical issues need attention', description: 'Start with findings that affect crawling and indexing, then rerun the audit after fixes.' };
}
/** @returns {Array<object>} Earned points and a muted remainder, always totalling 100. */
function getRows() {
return [
{ label: 'Audit score', value: score.value, color: rating.value.color },
{ label: 'Remaining points', value: 100 - score.value, color: `color-mix(in oklab, var(--${rating.value.color}) 14%, var(--canvas))` },
];
}
</script>
<template>
<div class="w-full space-y-7">
<div class="grid items-center gap-8 sm:grid-cols-[14rem_1fr]">
<DomDonutChart title="Technical SEO score" class="mx-auto max-w-56" :rows="rows" :height="192" :arc-width="12" :legend="false" :show-table="false">
<template #center>
<div :style="{ color: `var(--${rating.color})` }" role="img" :aria-label="`${score} out of 100. ${rating.label}.`">
<span class="block text-6xl font-medium tracking-tight tabular-nums" aria-hidden="true">{{ score }}</span>
<span class="mt-2 block text-xs text-muted-fg" aria-hidden="true">out of 100</span>
</div>
</template>
</DomDonutChart>
<div class="space-y-3">
<p class="text-xs font-medium uppercase tracking-wider text-muted-fg">Overall audit · Example report</p>
<p class="inline-flex items-center gap-2 text-sm font-semibold" :style="{ color: `var(--${rating.color})` }"><span class="size-2 rounded-full bg-current" aria-hidden="true" />{{ rating.label }}</p>
<h3 class="text-xl font-semibold tracking-tight">{{ rating.heading }}</h3>
<p class="max-w-lg text-sm leading-6 text-muted-fg">{{ rating.description }}</p>
</div>
</div>
<div class="space-y-4 border-t border-border pt-5">
<label class="flex flex-wrap items-center gap-4 text-sm">
<span class="font-medium">Preview score</span>
<input v-model.number="score" type="range" min="0" max="100" step="1" class="min-w-32 flex-1 cursor-pointer accent-primary" />
<output class="w-14 text-right text-xs tabular-nums text-muted-fg">{{ score }} / 100</output>
</label>
<div class="flex flex-wrap gap-x-5 gap-y-2 text-xs text-muted-fg">
<span class="inline-flex items-center gap-2"><span class="size-2 rounded-full bg-destructive" aria-hidden="true" />0–49 Poor</span>
<span class="inline-flex items-center gap-2"><span class="size-2 rounded-full bg-warning" aria-hidden="true" />50–79 Needs improvement</span>
<span class="inline-flex items-center gap-2"><span class="size-2 rounded-full bg-success" aria-hidden="true" />80–100 Good</span>
</div>
</div>
</div>
</template>
SEO audit category scores
Compact scores for the main areas of a technical audit. Green begins at 80, amber at 50, and red covers scores below 50.
Good
Good
Needs improvement
Poor
npm install @getdom/studio<script setup>
import '@getdom/studio/style.css';
import { DomDonutChart } from '@getdom/studio';
const categories = [
{ label: 'Crawlability', score: 92 },
{ label: 'Indexability', score: 80 },
{ label: 'Metadata', score: 67 },
{ label: 'Structured data', score: 42 },
].map(createCategory);
/**
* Presents an application-provided category score using the audit rating bands.
* @param {{label: string, score: number}} category Example audit category.
* @returns {object} Labelled score and donut segments totalling 100.
*/
function createCategory(category) {
const color = category.score >= 80 ? 'success' : category.score >= 50 ? 'warning' : 'destructive';
const status = category.score >= 80 ? 'Good' : category.score >= 50 ? 'Needs improvement' : 'Poor';
return {
...category, color, status,
rows: [
{ label: 'Audit score', value: category.score, color },
{ label: 'Remaining points', value: 100 - category.score, color: `color-mix(in oklab, var(--${color}) 14%, var(--canvas))` },
],
};
}
</script>
<template>
<div class="grid w-full gap-4 sm:grid-cols-2 xl:grid-cols-4">
<div v-for="category in categories" :key="category.label" class="min-w-0 rounded-xl border border-border p-4">
<DomDonutChart :title="category.label" :rows="category.rows" :height="148" :arc-width="10" :legend="false" :show-table="false">
<template #center>
<div :style="{ color: `var(--${category.color})` }" role="img" :aria-label="`${category.score} out of 100. ${category.status}.`">
<span class="block text-4xl font-medium tracking-tight tabular-nums" aria-hidden="true">{{ category.score }}</span>
<span class="mt-1 block text-xs text-muted-fg" aria-hidden="true">out of 100</span>
</div>
</template>
</DomDonutChart>
<p class="mt-4 text-center text-xs font-medium" :style="{ color: `var(--${category.color})` }">{{ category.status }}</p>
</div>
</div>
</template>
Working with your data
Supply one row per category. Values must be non-negative. Zero values remain in the data table without creating artificial slices. Missing values appear as a dash; the centre total includes visible finite values.
For score rings, supply the score and its remainder to 100, then use the centre slot to show the score. These examples use application-defined SEO bands: 80–100 good, 50–79 needs improvement, and 0–49 poor. The audit service owns the scoring calculation; category scores are not necessarily equally weighted.
Explore Data VisualisationReference
Props
Control props
| Name | Type | TS | Default | Description |
|---|---|---|---|---|
rows | array | Array<unknown> | [] | Application-owned records. Missing measurements remain gaps; zero is valid. |
title | string | string | 'Chart' | Visible title and accessible chart name. |
description | string | string | '' | Supporting context, units, or reporting period. |
height | number | number | 280 | Plot height in pixels; width responds to its container. |
format | 'number' | 'compact' | 'currency' | 'percent' | string | 'number' | Value format. Percent expects fractions, such as 0.25 for 25%. |
currency | string | string | 'GBP' | ISO currency code for currency formatting. |
locale | string | string | 'en-GB' | Locale used for number and date labels. |
valueFormatter | function | Function | — | Optional (value) => string override shared by axes, tooltips, and tables. |
loading | boolean | boolean | false | Show a loading state in the reserved plot space. |
error | string | string | '' | Show an application-owned error with a retry action. |
emptyLabel | string | string | 'No data for this period' | Message when no finite measurements are available. |
legend | boolean | boolean | true | Show buttons to toggle series without changing their colours. |
showTable | boolean | boolean | true | Offer an accessible, server-rendered table of the chart data. |
labelKey | string | string | 'label' | Unique category label field; supply one row per category. |
valueKey | string | string | 'value' | Non-negative measurement field. |
colorKey | string | string | 'color' | Optional field containing a semantic token or CSS colour. |
totalLabel | string | string | 'Total' | Caption beneath the sum of visible categories. |
arcWidth | number | number | 36 | Ring thickness in pixels. |
Auto-generated from Donut chart.props and inline _edit hints.
Slots
| Name | Scope | Description |
|---|---|---|
| #center | { total, formattedTotal } | Replaces the centre labels with custom content, such as an audit score out of 100. Keep content small enough to fit inside the ring. |
Events
| Name | Payload | Description |
|---|---|---|
| @select | { row, index } | Original record for the selected category. |
| @retry | — | Requests application data recovery. |
Names auto-detected from defineEmits and source emit() calls; payload and description from __doc.events when present.