THE BIG PICTURE
RevoGrid Charts JavaScript · TypeScript
JavaScript charts.
Bring your data to life.
Build interactive dashboards with a JavaScript and TypeScript chart library for React, Vue, Angular, and vanilla applications. Compare performance, spot trends, and explore your data.
Vanilla JavaScript / React / Vue / Angular
THE CONTRIBUTION
Where revenue comes from.
Enterprise mix is expanding.52% of revenue, up 6 pp from the 46% plan.
THE COMPARISON
Every region. Against plan.
One chart. Or a whole story.
Start with a standalone chart. Compose a dashboard as your questions grow.
Your app. Your visual language.
Bring your colors, labels, and theme. Keep every visualization feeling consistent.
From seeing to exploring.
Take the next step with Pivot: connect charts to reports, filters, and aggregated data.
See the whole story
From one chart to a decision.
Explore three runnable dashboards. Each uses one fictional source dataset, with explicit filters that update complementary views. Choose a story, change a filter and inspect the data.
Did a deployment worsen latency everywhere, or only for one service and region?
272 requests · 10 errors / 272 requests · 3.7% errors · raw p95 972 ms. Full 30-minute period.
Fictional requests, 18 October 2026, 12:00–12:30 UTC. Release B starts at 12:15. Filter by service and region to isolate the change. Trend and request charts share a time window.
When did errors increase?
Count columns and error-percent line use separate named value axes.
What does the latency tail hide?
Fixed 0, 100, 200, 400, 800, 1600, 3200 ms edges. Every selected request belongs to exactly one bin.
Which service and bucket?
Pick a populated cell to focus its service and five-minute window. Empty buckets have no requests.
Inspect a real request
Each point retains its request ID. Click, tap, or focus a point and activate it.
Activate a scatter request for raw details, or a heatmap cell for an explicit aggregate filter.
Runnable source and aggregation policy
All observations are loaded locally. Error percentage uses selected 5xx requests / selected requests; p95 uses nearest rank over raw durations. The host owns shared filters, and only the compatible elapsed-time axes are linked.
Reliability: mount a chart
Copy the source module below and give the host an explicit height. Call this after the host mounts:
import { createChart } from '@revolist/charts'
import { reliabilityExamples, selectRequests } from './reliability-data'
const examples = reliabilityExamples(selectRequests())
const chart = await createChart({
container: document.getElementById('chart')!,
chartType: 'columnLine',
...examples.trend,
})
// When the host unmounts:
// chart.destroy()Reliability: data and chart configurations
import { createChartDataset } from '@revolist/charts'
import type { StoryChartOptions } from './types'
export const services = ['Checkout', 'Search', 'Catalog'] as const
export const regions = ['Europe', 'Americas'] as const
export const reliabilityStart = 1792324800000 // 2026-10-18 12:00 UTC
export const bucketMs = 300_000
export const latencyBoundaries = [0, 100, 200, 400, 800, 1600, 3200]
export type RequestFact = {
requestId: string
startedAt: number
elapsedMs: number
service: string
region: string
durationMs: number
statusCode: number
deploymentId: string
}
/** Fictional deterministic requests; Catalog has no observations in minute 10–15. */
export const requests: RequestFact[] = services.flatMap((service, si) =>
regions.flatMap((region, ri) =>
Array.from({ length: 6 }, (_, bucket) =>
service === 'Catalog' && bucket === 2
? []
: Array.from({ length: 8 }, (_, sample) => {
const elapsedMs = bucket * bucketMs + sample * 37_000
const impacted = service === 'Checkout' && region === 'Europe' && bucket >= 3
const error = impacted ? sample % 3 === 0 : (sample + bucket + si + ri) % 19 === 0
return {
requestId: `req-${si}-${ri}-${bucket}-${sample}`,
startedAt: reliabilityStart + elapsedMs,
elapsedMs,
service,
region,
durationMs:
55 + si * 25 + ri * 15 + sample * 19 + (impacted ? 500 + sample * 120 : 0),
statusCode: error ? 503 : 200,
deploymentId: bucket >= 3 ? 'release-b' : 'release-a',
}
}),
).flat(),
),
)
export const isError = (request: RequestFact) => request.statusCode >= 500
export function selectRequests(service = 'all', region = 'all', bucket: number | null = null) {
return requests.filter(
request =>
(service === 'all' || request.service === service) &&
(region === 'all' || request.region === region) &&
(bucket === null || Math.floor(request.elapsedMs / bucketMs) === bucket),
)
}
export function reliabilitySummary(rows: RequestFact[]) {
const errors = rows.filter(isError).length
const sorted = rows.map(row => row.durationMs).sort((a, b) => a - b)
return {
count: rows.length,
errors,
errorPercent: rows.length ? (errors / rows.length) * 100 : null,
// Nearest-rank p95 over raw selected requests, never an average of percentiles.
p95: sorted.length ? sorted[Math.ceil(sorted.length * 0.95) - 1] : null,
}
}
export function reliabilityExamples(rows: RequestFact[]) {
const buckets = Array.from({ length: 6 }, (_, index) => {
const summary = reliabilitySummary(
rows.filter(row => Math.floor(row.elapsedMs / bucketMs) === index),
)
return {
elapsed: index * bucketMs,
label: `${index * 5} min`,
count: summary.count,
errorPercent: summary.errorPercent,
}
})
const trend: StoryChartOptions = {
dataset: createChartDataset(buckets, {
category: { field: 'elapsed', label: 'label' },
series: [
{ field: 'count', label: 'Requests' },
{ field: 'errorPercent', label: 'Errors · %' },
],
}),
bindings: {},
axes: {
x: { scale: 'linear', min: 0, max: 1_800_000, title: 'Elapsed · ms', unit: 'elapsed-ms' },
y: { min: 0, title: 'Request count' },
yAxes: { errors: { position: 'right', min: 0, max: 100, title: 'Errors · %' } },
},
seriesOverrides: {
count: { type: 'column', color: '#6647f0' },
errorPercent: { type: 'line', yAxis: 'errors', color: '#ff6b26', markers: true },
},
navigation: { enabled: true },
presentation: {
legendPosition: 'top',
description:
'Five-minute request counts and error percentage, with independent count and percentage axes.',
},
}
const histogram: StoryChartOptions = {
dataset: createChartDataset(rows, {
category: { field: 'requestId' },
series: [{ field: 'durationMs', label: 'Request latency' }],
}),
bindings: { value: ['durationMs'] },
chartOptions: { histogramBinBoundaries: latencyBoundaries, histogramAggregation: 'count' },
axes: { x: { scale: 'linear', title: 'Latency · ms' }, y: { title: 'Requests' } },
presentation: {
legendPosition: 'none',
description:
'Raw request latency distribution with fixed millisecond boundaries; bins are aggregates.',
},
}
const visibleServices = services.filter(service => rows.some(row => row.service === service))
const heatRows = visibleServices.map(service => ({
service,
...Object.fromEntries(
buckets.map((_, index) => {
const summary = reliabilitySummary(
rows.filter(
row => row.service === service && Math.floor(row.elapsedMs / bucketMs) === index,
),
)
return [`bucket-${index}`, summary.errorPercent]
}),
),
}))
const heatmap: StoryChartOptions = {
dataset: createChartDataset(heatRows, {
category: { field: 'service' },
series: buckets.map((_, index) => ({
field: `bucket-${index}`,
label: `${index * 5}–${index * 5 + 5} min`,
})),
}),
bindings: {},
axes: { matrix: { rows: { title: 'Service' }, columns: { title: 'Five-minute bucket' } } },
presentation: {
legendPosition: 'none',
description:
'Error percentage by service and five-minute bucket. Missing requests produce null cells, not zero-percent errors.',
},
}
const scatter: StoryChartOptions = {
dataset: createChartDataset(rows, {
category: { field: 'requestId' },
series: [
{ field: 'elapsedMs', label: 'Elapsed · ms' },
{ field: 'durationMs', label: 'Latency · ms' },
],
}),
bindings: { x: ['elapsedMs'], y: ['durationMs'] },
axes: {
x: { scale: 'linear', min: 0, max: 1_800_000, title: 'Elapsed · ms', unit: 'elapsed-ms' },
y: { title: 'Latency · ms', min: 0 },
},
navigation: { enabled: true, axes: 'x' },
presentation: {
legendPosition: 'none',
description:
'One genuine request per point: elapsed milliseconds versus request latency. Activate a point to inspect the original request.',
},
}
return { trend, histogram, heatmap, scatter }
}Reliability: shared option type
import type { ChartOptions } from '@revolist/charts'
/** Declarative example input; LiveChart owns the mounted chart and cleanup. */
export type StoryChartOptions = Pick<
ChartOptions,
| 'dataset'
| 'bindings'
| 'presentation'
| 'axes'
| 'seriesOverrides'
| 'chartOptions'
| 'rendererOptions'
| 'annotations'
| 'navigation'
| 'viewport'
| 'interaction'
| 'rangeSelection'
>Reliability: host filters and lifecycle
The running example's Vue component owns filters, record inspection and cleanup:
<template>
<div class="service-reliability" data-story="reliability">
<p>Did a deployment worsen latency everywhere, or only for one service and region?</p>
<div class="story-controls">
<label
>Service
<select v-model="service" aria-label="Service" @change="clearInspection">
<option value="all">All services</option>
<option v-for="item in services" :key="item">{{ item }}</option>
</select></label
>
<label
>Region
<select v-model="region" aria-label="Region" @change="clearInspection">
<option value="all">All regions</option>
<option v-for="item in regions" :key="item">{{ item }}</option>
</select></label
>
<button type="button" @click="reset">Reset filters</button>
</div>
<p class="story-summary" aria-live="polite">
{{ summary.count }} requests · {{ summary.errors }} errors / {{ summary.count }} requests ·
{{ percent(summary.errorPercent) }} errors · raw p95 {{ summary.p95 ?? '—' }} ms.
{{
bucket === null
? 'Full 30-minute period.'
: `Bucket ${bucket * 5}–${bucket * 5 + 5} minutes.`
}}
</p>
<p class="story-note">
Fictional requests, 18 October 2026, 12:00–12:30 UTC. Release B starts at 12:15. Filter by
service and region to isolate the change. Trend and request charts share a time window.
</p>
<div class="story-grid">
<section class="story-card">
<h4>When did errors increase?</h4>
<p>Count columns and error-percent line use separate named value axes.</p>
<div class="story-chart">
<LiveChart
:key="`trend-${revision}`"
type="columnLine"
:example="examples.trend"
eager
@ready="connect('trend', $event)"
@activate="aggregateTrend"
/>
</div>
</section>
<section class="story-card">
<h4>What does the latency tail hide?</h4>
<p>
Fixed 0, 100, 200, 400, 800, 1600, 3200 ms edges. Every selected request belongs to
exactly one bin.
</p>
<div class="story-chart">
<LiveChart
type="histogram"
:example="examples.histogram"
eager
@activate="histogramInspection"
/>
</div>
</section>
<section class="story-card">
<h4>Which service and bucket?</h4>
<p>
Pick a populated cell to focus its service and five-minute window. Empty buckets have no
requests.
</p>
<div class="story-chart">
<LiveChart type="heatmap" :example="examples.heatmap" eager @activate="inspectCell" />
</div>
<button v-if="cell" class="story-action" type="button" @click="applyCell">
Use {{ cell.service }} and {{ cell.bucket * 5 }}–{{ cell.bucket * 5 + 5 }} min
</button>
</section>
<section class="story-card">
<h4>Inspect a real request</h4>
<p>Each point retains its request ID. Click, tap, or focus a point and activate it.</p>
<div class="story-chart">
<LiveChart
:key="`scatter-${revision}`"
type="scatter"
:example="examples.scatter"
eager
@ready="connect('scatter', $event)"
@activate="inspectRequest"
/>
</div>
</section>
</div>
<p class="story-summary" aria-live="polite">{{ inspection }}</p>
<details v-if="selectedRequest" open class="story-source">
<summary>Request {{ selectedRequest.requestId }}</summary>
<dl>
<dt>Service / region</dt>
<dd>{{ selectedRequest.service }} / {{ selectedRequest.region }}</dd>
<dt>UTC instant</dt>
<dd>{{ new Date(selectedRequest.startedAt).toISOString() }}</dd>
<dt>Latency / response</dt>
<dd>{{ selectedRequest.durationMs }} ms / HTTP {{ selectedRequest.statusCode }}</dd>
<dt>Deployment</dt>
<dd>{{ selectedRequest.deploymentId }}</dd>
</dl>
</details>
</div>
</template>
<script setup lang="ts">
import { computed, onBeforeUnmount, ref, watch } from 'vue'
import { createChartSyncGroup, type ChartRef, type ChartNodeClick } from '@revolist/charts'
import LiveChart from '../LiveChart.vue'
import {
services,
regions,
requests,
selectRequests,
reliabilitySummary,
reliabilityExamples,
type RequestFact,
} from './reliability-data'
const service = ref('all'),
region = ref('all'),
bucket = ref<number | null>(null),
revision = ref(0)
const selected = computed(() => selectRequests(service.value, region.value, bucket.value))
const summary = computed(() => reliabilitySummary(selected.value))
const examples = computed(() => reliabilityExamples(selected.value))
const selectedRequest = ref<RequestFact>()
const cell = ref<{ service: string; bucket: number }>()
const inspection = ref(
'Activate a scatter request for raw details, or a heatmap cell for an explicit aggregate filter.',
)
const percent = (value: number | null) => (value === null ? '—' : `${value.toFixed(1)}%`)
let sync = createChartSyncGroup({ viewport: 'x', domain: false, hover: false })
const disconnect = new Map<string, () => void>()
function connect(key: string, chart: ChartRef) {
disconnect.get(key)?.()
disconnect.set(key, sync.connect(chart))
}
function clearInspection() {
selectedRequest.value = undefined
cell.value = undefined
inspection.value = 'Host filters updated; inspect a request or an aggregate.'
}
function reset() {
service.value = 'all'
region.value = 'all'
bucket.value = null
revision.value++
clearInspection()
}
function inspectRequest(detail: ChartNodeClick) {
const request = requests.find(row => row.requestId === detail.category.path[0])
if (!request || !selected.value.includes(request)) return
selectedRequest.value = request
inspection.value = `Raw request ${request.requestId}: ${request.durationMs} ms, HTTP ${request.statusCode}, ${request.service}, ${request.region}.`
}
function inspectCell(detail: ChartNodeClick) {
const index = Number(detail.series.id.replace('bucket-', ''))
const selectedService = String(detail.category.path[0])
if (
!services.includes(selectedService as (typeof services)[number]) ||
!Number.isInteger(index) ||
index < 0 ||
index > 5
)
return
selectedRequest.value = undefined
cell.value = { service: selectedService, bucket: index }
inspection.value = `Aggregate: ${selectedService}, ${index * 5}–${index * 5 + 5} min, ${percent(detail.value)} errors. Apply the explicit filter below the heatmap.`
}
function applyCell() {
if (!cell.value) return
service.value = cell.value.service
bucket.value = cell.value.bucket
clearInspection()
}
function aggregateTrend() {
selectedRequest.value = undefined
inspection.value =
'Five-minute aggregate: count and error percentage use the selected requests. This mark is not an individual request.'
}
function histogramInspection() {
selectedRequest.value = undefined
inspection.value =
'Latency bin: an aggregate over the selected raw requests. Use the scatter to inspect individual requests.'
}
watch(
selected,
() => {
disconnect.forEach(stop => stop())
disconnect.clear()
sync.destroy()
sync = createChartSyncGroup({ viewport: 'x', domain: false, hover: false })
},
{ flush: 'sync' },
)
onBeforeUnmount(() => {
disconnect.forEach(stop => stop())
sync.destroy()
})
</script>Find your perspective
A chart for every question.
Start with the question you need to answer. Explore a few different ways to see the story in your data.
Which option performs best?
Compare results side by side, rank the leaders, and see where the biggest differences are.
Grouped column
Compare values side by side across categories.
Grouped bar
Compare categories with horizontal bars.
Lollipop
Compare single values with compact stems and endpoints.
How is performance changing over time?
Follow the trend, see how contributions evolve, and compare multiple measures in one view.
Smooth line
Show trends with a smooth curve through each value.
Stacked area
Show how multiple measures contribute to a changing total.
Column and line
Compare measures using columns and lines, such as Sales as columns and Profit as a line.
What's driving the total?
Break a headline number into its parts, then explain the gains and losses behind it.
Donut
Show part-to-whole values in one or more rings.
Waterfall
Show how positive and negative changes build to a result.
Waffle
Compare part-to-whole values on a compact 100-cell grid.
Where is the value in a complex portfolio?
See the big contributors and explore how smaller groups fit into the bigger picture.
Treemap
Compare hierarchical values as proportional nested tiles.
Sunburst
Explore the current hierarchy level as proportional radial sectors.
Circle packing
Explore hierarchy through proportional nested circles.
Which factors move together?
Compare two measures, add a sense of scale, and spot relationships worth investigating.
Bubble
Compare two measures while bubble area represents a third measure.
Quadrant chart
Compare observations across four decision regions.
Connected scatter
Show the ordered path between paired X/Y observations.
What's typical, and what stands out?
Look beyond averages to understand the spread, find clusters, and identify unusual results.
Histogram
Summarize one measure in deterministic numeric bins.
Violin plot
Compare distribution shape and concentration across categories.
Box plot
Summarize a distribution with quartiles, whiskers, and outliers.
When do patterns repeat?
Reveal busy periods, quiet stretches, and recurring patterns across dates and categories.
Calendar heatmap
Show one daily measure across calendar weeks.
Heatmap
Compare measures across categories through color intensity.
Rose
Compare category magnitude through radial sector length.
Are we on track to hit our targets?
Put actual results against a goal and make progress easy to read at a glance.
Bullet
Compare actual values with explicit targets.
Gauge
Compare an actual value with its target on a radial scale.
Progress gauge
Show progress toward a target on a radial track.
Where does the process lose momentum?
Compare stages and inspect flows between groups to find the next opportunity for improvement.
Funnel
Show sequential stages that narrow toward an outcome.
Sankey
Trace weighted flow from categories to connected measures.
Chord
Compare weighted relationships through circular ribbons.
Where do plans overlap?
Compare start and end ranges to see timing, duration, and overlapping windows.
Timeline range
Compare start-to-end intervals across categories.
Range bar
Compare lower-to-upper intervals as horizontal bars.
What's behind a market move?
Read opening and closing prices, inspect the trading range, and compare price with volume.
Candlestick
Compare open, high, low, and close values over time.
Volume and price
Compare price movement with trading volume.
Taking your data places?
Choropleth, proportional symbol, and geographic heatmaps are available through the optional geographic capability pack, using your application's map provider and assets.
Give a moment in your data its own style.
Highlight a decline, a recovery or an unusual observation with authored segment and datum styles. The values and source identities stay the same. Toggle the styles to compare.
A dashed decline. A gradient recovery.
A diamond marks March; directed segment rules style February → March and March → April.
View line styling code
encoding: {
datums: [{ categoryId: marchId,
style: { marker: 'diamond', markerSize: 9 } }],
segments: [{ from: februaryId, to: marchId,
style: { stroke: '#c2410c', strokeDasharray: '6 4' } }],
}One observation, a different finish.
March uses a diagonal pattern; June uses a gradient. Every bar retains its raw value.
View column styling code
encoding: {
datums: [{ categoryId: marchId, style: {
fill: { type: 'pattern', pattern: 'diagonal',
color: '#7c3aed', size: 8 },
} }],
}Rules use actual dataset IDs and are saved with the chart. Segment rules apply to line and area outlines; marker policies apply to supported line and scatter glyphs. Custom datum encoding uses individual SVG marks, with an explicit fallback from batched SVG or WebGL. Explore styling options.
See your data change.
Line, grouped column and grouped bar charts animate data updates for 180 ms with ease-out by default. Compare the same update with motion switched off. Initial rendering is immediate; your device’s reduced-motion preference is respected.
Dataset 1Default animation
View animated chart code
// No transition options needed
const chart = await createChart(options)
await chart.update({ data: nextRows })Explicit opt-out
View immediate chart code
const chart = await createChart({
...options,
rendererOptions: { transitions: false },
})
await chart.update({ data: nextRows }) Override timing with transitions: { duration: 300, easing: 'linear' } in rendererOptions. Other chart families, batched SVG and WebGL remain immediate. Resize cancels motion; image and vector exports settle to committed data.
Build and customize.
Explore the standalone Charts guides for configuration, supported interactions and renderer limitations.
Better together
The chart starts the conversation.
Pivot takes it further.
Group and aggregate records in a Pivot table, then explore linked charts that follow the report's filters. See the dashboard that brings it all together.
Try the live dashboardFAQ
JavaScript charts — common questions
Make your next dashboard a little more insightful.
Explore the chart dashboard, or discover how Charts and Pivot fit into your application.

