Chatwoot Viz
Use @chatwoot/viz to add small, responsive charts to Vue 3.5+ applications.
The package exports BarChart, DonutChart, LineChart, PercentageChart,
HeatmapChart, and SankeyChart.
Agent protocol
Before writing chart code:
- Inspect the consuming project's Vue version, package manager, component
conventions, design tokens, and test setup.
- Confirm that the project uses Vue 3.5 or newer. Do not add this package to
React, Svelte, server-rendered templates without Vue, or Vue 2 projects.
- Reuse existing application data and tokens. Transform data in a computed
value instead of duplicating or mutating source records.
- Import the package stylesheet exactly once in the application's global
entry point.
- Give every chart a specific
aria-label describing the metric and grouping.
- Verify the chart at narrow and wide container sizes and test any item-click
behavior with both pointer and keyboard interaction.
Use the project's existing package manager:
pnpm add @chatwoot/viz
npm install @chatwoot/viz
yarn add @chatwoot/viz
bun add @chatwoot/viz
Import the stylesheet once, typically in main.js, main.ts, or the existing
global CSS entry:
import '@chatwoot/viz/style.css'
Import only the components needed by the view:
import {
BarChart,
DonutChart,
HeatmapChart,
LineChart,
PercentageChart,
SankeyChart,
} from '@chatwoot/viz'
Choose a chart
| Component |
Use for |
Avoid when |
BarChart |
Comparing values across discrete categories; grouped or stacked totals |
The primary task is reading change over a dense timeline |
DonutChart |
Circular part-to-whole breakdowns with a useful center |
Precise comparison across many small segments is primary |
LineChart |
Trends across ordered categories shared by one or more series |
Categories are unrelated or cumulative composition matters most |
PercentageChart |
Compact part-to-whole breakdowns in a single bar |
Values can be negative or do not share one meaningful total |
HeatmapChart |
Values at row/column intersections, density, schedules, cohorts |
Exact values must be compared without hover/focus |
SankeyChart |
Directed flows between stages and outcomes |
The graph contains cycles or links do not represent positive flow |
Do not use a chart when a compact table or a single statistic communicates the
result more clearly.
Cartesian charts
BarChart and LineChart use the same base data shape:
<script setup>
import { computed } from 'vue'
import { LineChart } from '@chatwoot/viz'
const props = defineProps({ report: { type: Object, required: true } })
const chartData = computed(() => ({
categories: props.report.periods.map((period) => period.label),
series: [
{
id: 'handled',
label: 'Handled',
color: 'var(--color-border-strong)',
data: props.report.periods.map((period) => period.handled),
},
{
id: 'resolved',
label: 'Resolved',
color: 'var(--color-primary)',
data: props.report.periods.map((period) => period.resolved),
},
],
}))
</script>
<template>
<LineChart :data="chartData" aria-label="Handled and resolved conversations by week" />
</template>
Rules for Cartesian data:
- Put x-axis values in
categories.
- Put each metric in
series; give every series a stable id and human label.
- Keep every series'
data aligned by category index.
- Use a number for a simple point. Use
{ value, ...metadata } when click
handlers need the original record or other metadata.
value and y are both accepted as the numeric field on point objects.
- Point objects may include an optional
description for muted supporting text in tooltips.
- Missing and non-numeric line points break the line. Missing and non-numeric
bar points are skipped.
- Set
timeseries on BarChart when categories are ordered dates or periods;
it reduces label density responsively.
- Use
stacked on BarChart only when adding series is meaningful. Positive
and negative values form separate stacks.
Useful props:
| Prop |
Components |
Behavior |
formatValue |
Bar, Line |
Function, suffix string such as "%", or template such as "{value} conversations" |
showTooltip |
Bar, Line |
Rich category tooltip; bind :show-tooltip="false" to disable |
showValues |
Bar, Line |
Bar defaults to false; Line defaults to true |
yDomain |
Bar, Line |
Explicit [minimum, maximum]; otherwise inferred |
yTicks |
Bar, Line |
Explicit tick values inside the domain |
yStepSize |
Bar, Line |
Positive tick interval or function receiving { min, max, values, tickCount } |
yTickCount |
Bar, Line |
Preferred inferred tick count; defaults to 5 |
height |
Bar, Line |
SVG view-box height; defaults to 360 |
barGap, barRadius, maxBarWidth |
Bar |
Tune grouped/stacked bar geometry; barRadius defaults to 4 |
pointRadius, xInset |
Line |
Tune markers and horizontal plot inset |
Scale options use yTicks, then yStepSize, then automatic ticks based on
yTickCount. A step rounds inferred domain bounds outward while an explicit
yDomain remains unchanged. Prefer an inferred domain. Add yDomain, yTicks,
or yStepSize only when the product requires an exact, comparable scale.
Zero-only data uses a non-negative 0 to 1 inferred fallback domain.
Aggregate charts
PercentageChart and DonutChart accept the same raw, non-negative segment
values. Without total, their sum is treated as 100%. With a positive total,
each share is calculated against that capacity and a positive remainder is
rendered automatically as Unused.
<script setup>
import { PercentageChart } from '@chatwoot/viz'
const data = {
total: 500,
segments: [
{ id: 'documents', label: 'Documents', value: 100, color: '#e5484d' },
{ id: 'music', label: 'Music', value: 30, color: '#f5a623' },
{ id: 'apps', label: 'Apps', value: 120, color: '#2f80ed' },
],
}
const formatStorage = (value) => `${value} GB`
</script>
<template>
<PercentageChart :data="data" :format-value="formatStorage" aria-label="Storage usage by type">
<template #legend-item="{ color, formattedValue, label }">
<span class="legend-swatch" :style="{ backgroundColor: color }" aria-hidden="true" />
<span>{{ label }}</span>
<strong>{{ formattedValue }}</strong>
</template>
</PercentageChart>
</template>
Use the same data in a donut and keep center content in its scoped slot:
<DonutChart :data="data" :format-value="formatStorage" aria-label="Storage usage by type">
<template #center="{ used }">
<strong>{{ formatStorage(used) }} used</strong>
</template>
</DonutChart>
This produces 20%, 6%, and 24% supplied segments plus a derived 50%
unused segment. Tooltips show the formatted raw value and percentage, such as
250 GB · 50%. Raw values remain available in legend slots and item-click
payloads.
Aggregate rules:
- Values above an explicit total produce an error instead of being rescaled.
- Invalid and negative values are skipped. An inferred chart needs at least one
positive value; an explicit total can render as 100% unused.
formatValue formats raw values; formatPercentage formats computed
percentages. Layout retains full precision and display values round to at
most two decimal places.
- The default legend is predictable: color swatch, label, and formatted
percentage. Use the
legend-item slot for business-specific arrangements
such as raw storage values, rating icons, or supporting counts.
- Both charts retain the legend's
<ul> and <li> semantics. Slot props are
item, id, index, label, color, value, percentage,
formattedValue, formattedPercentage, description, and isRemainder.
- A segment object's optional
description renders as muted tooltip text and
is included in its accessible label. It uses the description field
directly; there is no custom description accessor.
- Keep headings, summaries, units, precision, icons, and other business
presentation in the consuming view. Use the
remainderLabel and
remainderColor props to customize the derived segment.
showTooltip and showLegend control both charts. Percentage geometry uses
barHeight (24), barGap (2), and barRadius (4). Donut geometry
uses diameter (200), thickness (24), a constant-width segmentGap (3), and
cornerRadius (2).
- Donut's optional
center slot receives total, used, remainder, and
hasExplicitTotal.
Heatmaps
Use client-provided row and column labels. The component does not parse dates,
calculate weekdays, apply timezones, or localize labels.
<script setup>
import { HeatmapChart } from '@chatwoot/viz'
const data = {
columns: [
{ id: '09', label: '09:00' },
{ id: '10', label: '10:00' },
{ id: '11', label: '11:00' },
],
rows: [
{
id: 'monday',
label: 'Monday',
description: 'Aug 10, 2026',
data: [2, { value: 8, ticketIds: [41, 42] }, null],
},
],
}
</script>
<template>
<HeatmapChart
:data="data"
:domain="[0, 10]"
aria-label="Conversation volume by weekday and hour"
/>
</template>
Use quantile coloring when a skewed distribution makes equal-width linear
levels uninformative:
<HeatmapChart
:data="data"
:quantiles="[0.2, 0.4, 0.6, 0.8, 0.9, 0.99]"
:colors="heatmapColors"
zero-color="var(--color-surface-subtle)"
aria-label="Conversation volume by weekday and hour"
/>
Heatmap rules:
columns may contain strings, numbers, or objects. Prefer objects with
stable id and display label.
- Each row supports
id, label, optional description, and data or
values.
- A cell may be a number or an object with
value or count.
- A cell object may specify
color with any CSS color or var(--token).
null, missing, and non-numeric cells render as empty, non-interactive cells.
- The color domain is inferred across numeric cells. Pass
domain when several
heatmaps must use the same linear scale.
- Pass percentile cut points from
0 to 1 through quantiles for a
data-relative scale. Quantile coloring takes precedence over domain. Values
outside that range are ignored; valid values are deduplicated and sorted.
- Each quantile cut point creates a bucket boundary. Supply one more color than
quantiles to make every bucket distinct. If fewer colors are supplied, the
last color handles overflow buckets.
- Use
zeroColor for an exact zero-value color. Do not add 0 to quantiles
for this purpose because the zeroth quantile is the sample minimum, not
necessarily zero. Setting zeroColor excludes zeroes from the quantile
calculation so they do not collapse the non-zero buckets.
colors defaults to five CSS-variable-aware colors. Cell-level colors and
cellColor take precedence over zeroColor and the shared palette.
cellHeight (32), cellMinWidth (28), gap (4), and
rowLabelWidth (120) control density. The matrix scrolls horizontally when
it cannot fit its container.
formatValue accepts the same function/string forms as Cartesian charts.
Sankey diagrams
<script setup>
import { SankeyChart } from '@chatwoot/viz'
const data = {
nodes: [
{ id: 'handled', label: 'Handled', count: 9, color: 'var(--color-primary)' },
{ id: 'resolved', label: 'Resolved', count: 3, color: '#038574' },
{ id: 'handoff', label: 'Handed off', count: 6, color: '#915930' },
],
links: [
{ source: 'handled', target: 'resolved', value: 3 },
{ source: 'handled', target: 'handoff', value: 6 },
],
}
</script>
<template>
<SankeyChart
:data="data"
:format-value="(value) => value.toLocaleString()"
aria-label="Conversation outcomes from handled conversations"
/>
</template>
Sankey rules:
- Give every node a unique
id; label, count/value, and color are
optional.
- Connect links with
source, target, and a positive value. An endpoint
may be a node id, zero-based node index, or node object.
- Keep the graph directed and acyclic.
- A node value is inferred from connected links when its own value is absent.
- A link without a color inherits its target node's color with reduced opacity.
- Unlike the other charts,
SankeyChart accepts only a function for
formatValue, not a suffix or template string.
- Use
nodeWidth (10), nodePadding (28), height (340), and
showLabelBackground to tune layout without rewriting SVG output.
Item interactions
Attach @item-click when selecting a visual item should navigate, filter, or
open details. Do not add separate click targets over the chart. The components
already support mouse, Enter, and Space interaction.
<script setup>
const emit = defineEmits(['select'])
function selectItem(payload) {
// payload.item, payload.category, and payload.series are original input data.
emit('select', payload)
}
</script>
<template>
<BarChart :data="data" @item-click="selectItem" />
</template>
Payloads:
| Chart |
Common payload fields |
Additional fields |
| Bar, Line |
item, value, formattedValue, event |
Original category and series; ids, labels, and indexes |
| Heatmap |
itemType: "cell", item, value, formattedValue, event |
Original row and column; ids, labels, descriptions, and indexes |
| Percentage, Donut |
item, value, formattedValue, event, index |
Calculated percentage, formatted percentage, description, id, label, and remainder state |
| Sankey node |
itemType: "node", item, value, formattedValue, event, index |
id, label |
| Sankey link |
itemType: "link", item, value, formattedValue, event, index |
Original source/target nodes plus their ids and labels |
Prefer point or cell objects when a handler needs metadata; item preserves
the original object. Keep navigation and application state changes in the
consumer's callback rather than inside transformed chart data.
Custom data accessors
Adapt existing application schemas with accessor props instead of cloning
records solely to rename fields:
<BarChart
:data="data"
:category-label="(category) => category.name"
:series-id="(series) => series.key"
:series-label="(series) => series.name"
:series-values="(series) => series.samples"
:point-value="(point) => point.total"
:series-color="(series) => series.fill"
/>
- Bar and Line:
categoryLabel, seriesId, seriesLabel, seriesValues,
pointValue, pointDescription, and color accessors. pointDescription defaults to the
point object's optional description field.
- Heatmap:
columnId, columnLabel, rowId, rowLabel, rowDescription,
rowValues, cellValue, and cellColor.
- Percentage and Donut:
segmentId, segmentLabel, segmentValue, and
segmentColor.
- Sankey:
nodeId, nodeLabel, nodeValue, nodeColor, linkValue, and
linkColor.
Responsiveness, accessibility, and theming
Bar, Line, and Sankey charts observe their container width and recalculate
their layout. Aggregate charts scale to their container with CSS. Give the
parent a real width and min-width: 0 when it is inside a flex or grid layout.
width on charts that accept it is a fallback before measurement, not normally
a fixed rendered width.
Use data-level colors for individual series, nodes, links, and cells. Use
--cw-viz-* CSS custom properties for shared presentation:
.analytics-chart {
--cw-viz-line-width: 2px;
--cw-viz-line-tooltip-background: var(--color-surface);
--cw-viz-bar-tooltip-background: var(--color-surface);
--cw-viz-heatmap-level-0-color: var(--color-surface-subtle);
--cw-viz-heatmap-level-4-color: var(--color-primary);
--cw-viz-donut-remainder-color: var(--color-surface-subtle);
--cw-viz-donut-tooltip-background: var(--color-surface);
--cw-viz-percentage-remainder-color: var(--color-surface-subtle);
--cw-viz-percentage-tooltip-background: var(--color-surface);
}
Do not remove focus styles, replace semantic buttons with click-only elements,
or use color as the only explanation of a metric. Keep labels concise and pass
a useful aria-label, even though every component has a generic default.
Common mistakes
| Mistake |
Fix |
| Importing only the component |
Import @chatwoot/viz/style.css once globally |
Passing show-tooltip="false" |
Bind the Boolean: :show-tooltip="false" |
| Using series arrays of different meaning/order |
Align every point to the same category index |
Calculating dates inside HeatmapChart |
Localize and label rows/columns in the client |
Adding 0 to heatmap quantiles for a zero bucket |
Pass zeroColor; keep quantiles as percentile cut points |
| Passing precomputed percentage labels |
Pass raw values and let aggregate charts calculate them |
| Letting percentage values exceed an explicit total |
Correct the values or increase the shared total |
| Passing zero/negative Sankey links or cyclic data |
Validate positive flows and a directed acyclic graph |
Passing format-value="%" to Sankey |
Pass a function: :format-value="(value) => String(value) + '%'" |
| Hard-coding chart width to make it responsive |
Size the container; let the chart's observer measure it |
| Rebuilding accessible click behavior outside the chart |
Use @item-click and the supplied payload |
| Mutating API data into the chart shape |
Derive chart data with computed |
Verification
After implementation:
- Run the consuming project's formatter, linter, tests, and production build.
- Confirm the number and order of categories, series, aggregate segments,
rows, columns, nodes, and links against the source data.
- Check empty, missing, zero, negative, and unusually large values relevant to
the selected chart.
- Resize the container below and above its normal width; check clipped labels,
tooltips, and heatmap scrolling.
- Focus interactive points/cells/segments/nodes/links and activate them with
Enter and Space. Confirm the handler receives the original input objects.
- Check that the chart has an accurate accessible name and remains readable
with the consuming application's light/dark theme tokens.
1---2name: chatwoot-viz3description: Build and modify Vue 3 data visualizations with @chatwoot/viz. Use when an application needs responsive bar charts, line charts, aggregate charts, heatmaps, or Sankey diagrams; when converting application data into the library's chart data shapes; or when implementing chart formatting, item-click interactions, accessibility, responsive sizing, or CSS-variable theming.4license: MIT5---67# Chatwoot Viz89Use `@chatwoot/viz` to add small, responsive charts to Vue 3.5+ applications.10The package exports `BarChart`, `DonutChart`, `LineChart`, `PercentageChart`,11`HeatmapChart`, and `SankeyChart`.1213## Agent protocol1415Before writing chart code:16171. Inspect the consuming project's Vue version, package manager, component18 conventions, design tokens, and test setup.192. Confirm that the project uses Vue 3.5 or newer. Do not add this package to20 React, Svelte, server-rendered templates without Vue, or Vue 2 projects.213. Reuse existing application data and tokens. Transform data in a computed22 value instead of duplicating or mutating source records.234. Import the package stylesheet exactly once in the application's global24 entry point.255. Give every chart a specific `aria-label` describing the metric and grouping.266. Verify the chart at narrow and wide container sizes and test any item-click27 behavior with both pointer and keyboard interaction.2829Use the project's existing package manager:3031```sh32pnpm add @chatwoot/viz33npm install @chatwoot/viz34yarn add @chatwoot/viz35bun add @chatwoot/viz36```3738Import the stylesheet once, typically in `main.js`, `main.ts`, or the existing39global CSS entry:4041```js42import '@chatwoot/viz/style.css'43```4445Import only the components needed by the view:4647```js48import {49 BarChart,50 DonutChart,51 HeatmapChart,52 LineChart,53 PercentageChart,54 SankeyChart,55} from '@chatwoot/viz'56```5758## Choose a chart5960| Component | Use for | Avoid when |61| ----------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------- |62| `BarChart` | Comparing values across discrete categories; grouped or stacked totals | The primary task is reading change over a dense timeline |63| `DonutChart` | Circular part-to-whole breakdowns with a useful center | Precise comparison across many small segments is primary |64| `LineChart` | Trends across ordered categories shared by one or more series | Categories are unrelated or cumulative composition matters most |65| `PercentageChart` | Compact part-to-whole breakdowns in a single bar | Values can be negative or do not share one meaningful total |66| `HeatmapChart` | Values at row/column intersections, density, schedules, cohorts | Exact values must be compared without hover/focus |67| `SankeyChart` | Directed flows between stages and outcomes | The graph contains cycles or links do not represent positive flow |6869Do not use a chart when a compact table or a single statistic communicates the70result more clearly.7172## Cartesian charts7374`BarChart` and `LineChart` use the same base data shape:7576```vue77<script setup>78import { computed } from 'vue'79import { LineChart } from '@chatwoot/viz'8081const props = defineProps({ report: { type: Object, required: true } })8283const chartData = computed(() => ({84 categories: props.report.periods.map((period) => period.label),85 series: [86 {87 id: 'handled',88 label: 'Handled',89 color: 'var(--color-border-strong)',90 data: props.report.periods.map((period) => period.handled),91 },92 {93 id: 'resolved',94 label: 'Resolved',95 color: 'var(--color-primary)',96 data: props.report.periods.map((period) => period.resolved),97 },98 ],99}))100</script>101102<template>103 <LineChart :data="chartData" aria-label="Handled and resolved conversations by week" />104</template>105```106107Rules for Cartesian data:108109- Put x-axis values in `categories`.110- Put each metric in `series`; give every series a stable `id` and human label.111- Keep every series' `data` aligned by category index.112- Use a number for a simple point. Use `{ value, ...metadata }` when click113 handlers need the original record or other metadata.114- `value` and `y` are both accepted as the numeric field on point objects.115- Point objects may include an optional `description` for muted supporting text in tooltips.116- Missing and non-numeric line points break the line. Missing and non-numeric117 bar points are skipped.118- Set `timeseries` on `BarChart` when categories are ordered dates or periods;119 it reduces label density responsively.120- Use `stacked` on `BarChart` only when adding series is meaningful. Positive121 and negative values form separate stacks.122123Useful props:124125| Prop | Components | Behavior |126| ------------------------------------ | ---------- | ------------------------------------------------------------------------------------ |127| `formatValue` | Bar, Line | Function, suffix string such as `"%"`, or template such as `"{value} conversations"` |128| `showTooltip` | Bar, Line | Rich category tooltip; bind `:show-tooltip="false"` to disable |129| `showValues` | Bar, Line | Bar defaults to `false`; Line defaults to `true` |130| `yDomain` | Bar, Line | Explicit `[minimum, maximum]`; otherwise inferred |131| `yTicks` | Bar, Line | Explicit tick values inside the domain |132| `yStepSize` | Bar, Line | Positive tick interval or function receiving `{ min, max, values, tickCount }` |133| `yTickCount` | Bar, Line | Preferred inferred tick count; defaults to `5` |134| `height` | Bar, Line | SVG view-box height; defaults to `360` |135| `barGap`, `barRadius`, `maxBarWidth` | Bar | Tune grouped/stacked bar geometry; `barRadius` defaults to `4` |136| `pointRadius`, `xInset` | Line | Tune markers and horizontal plot inset |137138Scale options use `yTicks`, then `yStepSize`, then automatic ticks based on139`yTickCount`. A step rounds inferred domain bounds outward while an explicit140`yDomain` remains unchanged. Prefer an inferred domain. Add `yDomain`, `yTicks`,141or `yStepSize` only when the product requires an exact, comparable scale.142Zero-only data uses a non-negative `0` to `1` inferred fallback domain.143144## Aggregate charts145146`PercentageChart` and `DonutChart` accept the same raw, non-negative segment147values. Without `total`, their sum is treated as 100%. With a positive `total`,148each share is calculated against that capacity and a positive remainder is149rendered automatically as `Unused`.150151```vue152<script setup>153import { PercentageChart } from '@chatwoot/viz'154155const data = {156 total: 500,157 segments: [158 { id: 'documents', label: 'Documents', value: 100, color: '#e5484d' },159 { id: 'music', label: 'Music', value: 30, color: '#f5a623' },160 { id: 'apps', label: 'Apps', value: 120, color: '#2f80ed' },161 ],162}163164const formatStorage = (value) => `${value} GB`165</script>166167<template>168 <PercentageChart :data="data" :format-value="formatStorage" aria-label="Storage usage by type">169 <template #legend-item="{ color, formattedValue, label }">170 <span class="legend-swatch" :style="{ backgroundColor: color }" aria-hidden="true" />171 <span>{{ label }}</span>172 <strong>{{ formattedValue }}</strong>173 </template>174 </PercentageChart>175</template>176```177178Use the same data in a donut and keep center content in its scoped slot:179180```vue181<DonutChart :data="data" :format-value="formatStorage" aria-label="Storage usage by type">182 <template #center="{ used }">183 <strong>{{ formatStorage(used) }} used</strong>184 </template>185</DonutChart>186```187188This produces `20%`, `6%`, and `24%` supplied segments plus a derived 50%189unused segment. Tooltips show the formatted raw value and percentage, such as190`250 GB · 50%`. Raw values remain available in legend slots and item-click191payloads.192193Aggregate rules:194195- Values above an explicit total produce an error instead of being rescaled.196- Invalid and negative values are skipped. An inferred chart needs at least one197 positive value; an explicit total can render as 100% unused.198- `formatValue` formats raw values; `formatPercentage` formats computed199 percentages. Layout retains full precision and display values round to at200 most two decimal places.201- The default legend is predictable: color swatch, label, and formatted202 percentage. Use the `legend-item` slot for business-specific arrangements203 such as raw storage values, rating icons, or supporting counts.204- Both charts retain the legend's `<ul>` and `<li>` semantics. Slot props are205 `item`, `id`, `index`, `label`, `color`, `value`, `percentage`,206 `formattedValue`, `formattedPercentage`, `description`, and `isRemainder`.207- A segment object's optional `description` renders as muted tooltip text and208 is included in its accessible label. It uses the `description` field209 directly; there is no custom description accessor.210- Keep headings, summaries, units, precision, icons, and other business211 presentation in the consuming view. Use the `remainderLabel` and212 `remainderColor` props to customize the derived segment.213- `showTooltip` and `showLegend` control both charts. Percentage geometry uses214 `barHeight` (`24`), `barGap` (`2`), and `barRadius` (`4`). Donut geometry215 uses `diameter` (`200`), `thickness` (`24`), a constant-width `segmentGap` (`3`), and216 `cornerRadius` (`2`).217- Donut's optional `center` slot receives `total`, `used`, `remainder`, and218 `hasExplicitTotal`.219220## Heatmaps221222Use client-provided row and column labels. The component does not parse dates,223calculate weekdays, apply timezones, or localize labels.224225```vue226<script setup>227import { HeatmapChart } from '@chatwoot/viz'228229const data = {230 columns: [231 { id: '09', label: '09:00' },232 { id: '10', label: '10:00' },233 { id: '11', label: '11:00' },234 ],235 rows: [236 {237 id: 'monday',238 label: 'Monday',239 description: 'Aug 10, 2026',240 data: [2, { value: 8, ticketIds: [41, 42] }, null],241 },242 ],243}244</script>245246<template>247 <HeatmapChart248 :data="data"249 :domain="[0, 10]"250 aria-label="Conversation volume by weekday and hour"251 />252</template>253```254255Use quantile coloring when a skewed distribution makes equal-width linear256levels uninformative:257258```vue259<HeatmapChart260 :data="data"261 :quantiles="[0.2, 0.4, 0.6, 0.8, 0.9, 0.99]"262 :colors="heatmapColors"263 zero-color="var(--color-surface-subtle)"264 aria-label="Conversation volume by weekday and hour"265/>266```267268Heatmap rules:269270- `columns` may contain strings, numbers, or objects. Prefer objects with271 stable `id` and display `label`.272- Each row supports `id`, `label`, optional `description`, and `data` or273 `values`.274- A cell may be a number or an object with `value` or `count`.275- A cell object may specify `color` with any CSS color or `var(--token)`.276- `null`, missing, and non-numeric cells render as empty, non-interactive cells.277- The color domain is inferred across numeric cells. Pass `domain` when several278 heatmaps must use the same linear scale.279- Pass percentile cut points from `0` to `1` through `quantiles` for a280 data-relative scale. Quantile coloring takes precedence over `domain`. Values281 outside that range are ignored; valid values are deduplicated and sorted.282- Each quantile cut point creates a bucket boundary. Supply one more color than283 quantiles to make every bucket distinct. If fewer colors are supplied, the284 last color handles overflow buckets.285- Use `zeroColor` for an exact zero-value color. Do not add `0` to `quantiles`286 for this purpose because the zeroth quantile is the sample minimum, not287 necessarily zero. Setting `zeroColor` excludes zeroes from the quantile288 calculation so they do not collapse the non-zero buckets.289- `colors` defaults to five CSS-variable-aware colors. Cell-level colors and290 `cellColor` take precedence over `zeroColor` and the shared palette.291- `cellHeight` (`32`), `cellMinWidth` (`28`), `gap` (`4`), and292 `rowLabelWidth` (`120`) control density. The matrix scrolls horizontally when293 it cannot fit its container.294- `formatValue` accepts the same function/string forms as Cartesian charts.295296## Sankey diagrams297298```vue299<script setup>300import { SankeyChart } from '@chatwoot/viz'301302const data = {303 nodes: [304 { id: 'handled', label: 'Handled', count: 9, color: 'var(--color-primary)' },305 { id: 'resolved', label: 'Resolved', count: 3, color: '#038574' },306 { id: 'handoff', label: 'Handed off', count: 6, color: '#915930' },307 ],308 links: [309 { source: 'handled', target: 'resolved', value: 3 },310 { source: 'handled', target: 'handoff', value: 6 },311 ],312}313</script>314315<template>316 <SankeyChart317 :data="data"318 :format-value="(value) => value.toLocaleString()"319 aria-label="Conversation outcomes from handled conversations"320 />321</template>322```323324Sankey rules:325326- Give every node a unique `id`; `label`, `count`/`value`, and `color` are327 optional.328- Connect links with `source`, `target`, and a positive `value`. An endpoint329 may be a node id, zero-based node index, or node object.330- Keep the graph directed and acyclic.331- A node value is inferred from connected links when its own value is absent.332- A link without a color inherits its target node's color with reduced opacity.333- Unlike the other charts, `SankeyChart` accepts only a function for334 `formatValue`, not a suffix or template string.335- Use `nodeWidth` (`10`), `nodePadding` (`28`), `height` (`340`), and336 `showLabelBackground` to tune layout without rewriting SVG output.337338## Item interactions339340Attach `@item-click` when selecting a visual item should navigate, filter, or341open details. Do not add separate click targets over the chart. The components342already support mouse, Enter, and Space interaction.343344```vue345<script setup>346const emit = defineEmits(['select'])347348function selectItem(payload) {349 // payload.item, payload.category, and payload.series are original input data.350 emit('select', payload)351}352</script>353354<template>355 <BarChart :data="data" @item-click="selectItem" />356</template>357```358359Payloads:360361| Chart | Common payload fields | Additional fields |362| ----------------- | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |363| Bar, Line | `item`, `value`, `formattedValue`, `event` | Original `category` and `series`; ids, labels, and indexes |364| Heatmap | `itemType: "cell"`, `item`, `value`, `formattedValue`, `event` | Original `row` and `column`; ids, labels, descriptions, and indexes |365| Percentage, Donut | `item`, `value`, `formattedValue`, `event`, `index` | Calculated percentage, formatted percentage, description, id, label, and remainder state |366| Sankey node | `itemType: "node"`, `item`, `value`, `formattedValue`, `event`, `index` | `id`, `label` |367| Sankey link | `itemType: "link"`, `item`, `value`, `formattedValue`, `event`, `index` | Original source/target nodes plus their ids and labels |368369Prefer point or cell objects when a handler needs metadata; `item` preserves370the original object. Keep navigation and application state changes in the371consumer's callback rather than inside transformed chart data.372373## Custom data accessors374375Adapt existing application schemas with accessor props instead of cloning376records solely to rename fields:377378```vue379<BarChart380 :data="data"381 :category-label="(category) => category.name"382 :series-id="(series) => series.key"383 :series-label="(series) => series.name"384 :series-values="(series) => series.samples"385 :point-value="(point) => point.total"386 :series-color="(series) => series.fill"387/>388```389390- Bar and Line: `categoryLabel`, `seriesId`, `seriesLabel`, `seriesValues`,391 `pointValue`, `pointDescription`, and color accessors. `pointDescription` defaults to the392 point object's optional `description` field.393- Heatmap: `columnId`, `columnLabel`, `rowId`, `rowLabel`, `rowDescription`,394 `rowValues`, `cellValue`, and `cellColor`.395- Percentage and Donut: `segmentId`, `segmentLabel`, `segmentValue`, and396 `segmentColor`.397- Sankey: `nodeId`, `nodeLabel`, `nodeValue`, `nodeColor`, `linkValue`, and398 `linkColor`.399400## Responsiveness, accessibility, and theming401402Bar, Line, and Sankey charts observe their container width and recalculate403their layout. Aggregate charts scale to their container with CSS. Give the404parent a real width and `min-width: 0` when it is inside a flex or grid layout.405`width` on charts that accept it is a fallback before measurement, not normally406a fixed rendered width.407408Use data-level colors for individual series, nodes, links, and cells. Use409`--cw-viz-*` CSS custom properties for shared presentation:410411```css412.analytics-chart {413 --cw-viz-line-width: 2px;414 --cw-viz-line-tooltip-background: var(--color-surface);415 --cw-viz-bar-tooltip-background: var(--color-surface);416 --cw-viz-heatmap-level-0-color: var(--color-surface-subtle);417 --cw-viz-heatmap-level-4-color: var(--color-primary);418 --cw-viz-donut-remainder-color: var(--color-surface-subtle);419 --cw-viz-donut-tooltip-background: var(--color-surface);420 --cw-viz-percentage-remainder-color: var(--color-surface-subtle);421 --cw-viz-percentage-tooltip-background: var(--color-surface);422}423```424425Do not remove focus styles, replace semantic buttons with click-only elements,426or use color as the only explanation of a metric. Keep labels concise and pass427a useful `aria-label`, even though every component has a generic default.428429## Common mistakes430431| Mistake | Fix |432| ------------------------------------------------------ | ----------------------------------------------------------------- |433| Importing only the component | Import `@chatwoot/viz/style.css` once globally |434| Passing `show-tooltip="false"` | Bind the Boolean: `:show-tooltip="false"` |435| Using series arrays of different meaning/order | Align every point to the same category index |436| Calculating dates inside `HeatmapChart` | Localize and label rows/columns in the client |437| Adding `0` to heatmap quantiles for a zero bucket | Pass `zeroColor`; keep quantiles as percentile cut points |438| Passing precomputed percentage labels | Pass raw values and let aggregate charts calculate them |439| Letting percentage values exceed an explicit total | Correct the values or increase the shared total |440| Passing zero/negative Sankey links or cyclic data | Validate positive flows and a directed acyclic graph |441| Passing `format-value="%"` to Sankey | Pass a function: `:format-value="(value) => String(value) + '%'"` |442| Hard-coding chart width to make it responsive | Size the container; let the chart's observer measure it |443| Rebuilding accessible click behavior outside the chart | Use `@item-click` and the supplied payload |444| Mutating API data into the chart shape | Derive chart data with `computed` |445446## Verification447448After implementation:4494501. Run the consuming project's formatter, linter, tests, and production build.4512. Confirm the number and order of categories, series, aggregate segments,452 rows, columns, nodes, and links against the source data.4533. Check empty, missing, zero, negative, and unusually large values relevant to454 the selected chart.4554. Resize the container below and above its normal width; check clipped labels,456 tooltips, and heatmap scrolling.4575. Focus interactive points/cells/segments/nodes/links and activate them with458 Enter and Space. Confirm the handler receives the original input objects.4596. Check that the chart has an accurate accessible name and remains readable460 with the consuming application's light/dark theme tokens.