Bar Chart

Compare categorical values using proportional bar lengths with vertical or horizontal orientation.

npm install @plocks/charts

Basics

Basic BarChart usage with a title.

Loading demo…

import { BarChart } from '@plocks/charts'; import { QUARTERLY_REVENUE } from './data'; export function Demo() { return ( <BarChart title="Quarterly revenue" subtitle="North America" h={260} data={QUARTERLY_REVENUE} barSpacing={0.25} barBorderRadius={6} valueFormatter={(value) => `$${(value / 1000).toFixed(0)}k`} xAxis={{ show: true }} yAxis={{ show: true, labelFormatter: (value) => `$${(value / 1000).toFixed(0)}k`, }} grid={{ show: true, style: 'dotted' }} tooltip={{ show: true, formatter: (point) => `${point.category}: $${point.value.toLocaleString()}`, }} enableCrosshair liveTooltip /> ); }

Diverging from zero

Key settings
  • colorScale={{ type: 'diverging', midpoint: 0, colors: [low, high] }} colors each bar by its value. The scale's domain always includes zero, where bars start.
  • Both arms are symmetric around the midpoint, so a $40k loss and a $40k gain carry the same intensity.

Loading demo…

import { BarChart } from '@plocks/charts'; import { NET_CASH_FLOW } from './data'; export function Demo() { return ( <BarChart title="Monthly net cash flow" subtitle="Diverging from zero: red months lost money, blue months made it" h={340} data={NET_CASH_FLOW} barSpacing={0.24} legend={{ show: false }} colorScale={{ type: 'diverging', midpoint: 0, colors: ['#e34948', '#2a78d6'] }} yAxis={{ show: true, title: 'Net cash flow (USD thousands)', labelFormatter: (value) => `$${value}k`, }} xAxis={{ show: true }} grid={{ show: true }} valueFormatter={(value) => `${value < 0 ? '-' : '+'}$${Math.abs(value)}k`} /> ); }

Gradient bars

Key settings
  • barColor takes a gradient as well as a color: { angle, stops, extent }.
  • extent: 'plot' lays one gradient across the whole plot, so each bar shows the slice behind it. The default, extent: 'mark', gives every bar the full gradient.

Loading demo…

import { BarChart } from '@plocks/charts'; import { HOURLY_REQUESTS } from './data'; export function Demo() { return ( <BarChart title="API requests by hour" subtitle="One gradient spans the plot, so busier hours reach the deeper end" h={320} data={HOURLY_REQUESTS} barSpacing={0.28} legend={{ show: false }} barBorderRadius={6} barColor={{ angle: 90, extent: 'plot', stops: [ { offset: 0, color: '#12805a' }, { offset: 1, color: '#8fdcbf' }, ], }} yAxis={{ show: true, title: 'Requests (millions)', labelFormatter: (value) => `${value}M`, }} xAxis={{ show: true }} grid={{ show: true }} valueFormatter={(value) => `${value}M requests`} /> ); }

Coloring with a function

Key settings
  • colorScale also accepts a function, ({ datum, series, seriesIndex, categoryIndex }) => color. Here each region is green when it beat its goal and red when it missed. Return undefined to fall back to barColor.
  • Green and red are status colors, so the value labels state every variance: color is never the only cue.

Loading demo…

import { BarChart } from '@plocks/charts'; import { REGIONAL_REVENUE } from './data'; // Status colors: the value labels spell out each variance, so color is never the only cue. const BEAT_PLAN = '#0ca30c'; const MISSED_PLAN = '#d03b3b'; const formatMillions = (value: number) => `$${value.toFixed(2)}M`; const formatVariance = (value: number, datum: (typeof REGIONAL_REVENUE)[number]) => { const goal = datum.data?.goal ?? 0; const diff = value - goal; const direction = diff >= 0 ? '+' : '-'; return `${direction}$${Math.abs(diff).toFixed(2)}M vs goal`; }; export function Demo() { return ( <BarChart title="Quarterly Revenue by Region" subtitle="Q3 actuals with variance to plan" h={420} data={REGIONAL_REVENUE} barSpacing={0.32} colorScale={({ datum }) => (datum.value >= (datum.data?.goal ?? 0) ? BEAT_PLAN : MISSED_PLAN)} legend={{ show: false }} valueFormatter={(value, datum) => { const goal = datum.data?.goal; return goal != null ? `${formatMillions(value)} actual (goal ${formatMillions(goal)})` : formatMillions(value); }} valueLabel={{ formatter: (value, datum) => formatVariance(value, datum as (typeof REGIONAL_REVENUE)[number]), color: '#1f2937', fontSize: 12, fontWeight: '600', offset: 12, }} yAxis={{ show: true, title: 'Revenue (USD millions)', titleFontSize: 12, labelFormatter: (value) => `$${value.toFixed(1)}M`, }} xAxis={{ show: true }} grid={{ show: true }} tooltip={{ formatter: (datum) => { const goal = datum.data?.goal ?? 0; const diff = datum.value - goal; const pct = goal ? (diff / goal) * 100 : 0; const direction = diff >= 0 ? '+' : '-'; const varianceValue = `${direction}${formatMillions(Math.abs(diff))}`; const variancePct = `${direction}${Math.abs(pct).toFixed(1)}%`; return [ `${datum.category}`, `Actual: ${formatMillions(datum.value)}`, `Goal: ${formatMillions(goal)}`, `Variance: ${varianceValue} (${variancePct})`, datum.data?.focus ? `Focus: ${datum.data.focus}` : undefined, ] .filter(Boolean) .join('\n'); }, }} /> ); }

Feature Adoption By Tier

Loading demo…

import { BarChart } from '@plocks/charts'; import { FEATURE_ADOPTION } from './data'; const formatAccounts = (value: number) => `${value.toLocaleString()} accounts`; export function Demo() { return ( <BarChart title="Feature adoption by customer tier" subtitle="Accounts live on the experimentation canvas within 45 days" h={400} data={FEATURE_ADOPTION} barSpacing={0.26} barBorderRadius={12} legend={{ show: false }} valueFormatter={(value, datum) => { const segmentTotal = datum.data?.accounts ?? value; const rate = datum.data?.adoptionRate ?? value / segmentTotal; const percentage = `${Math.round((rate ?? 0) * 100)}% adoption`; return `${formatAccounts(value)} (${percentage})`; }} valueLabel={{ position: 'inside', color: '#ffffff', fontSize: 13, fontWeight: '600', formatter: (value) => value.toLocaleString(), }} yAxis={{ show: true, title: 'Activated accounts', titleFontSize: 12, labelFormatter: (value) => value.toLocaleString(), }} xAxis={{ show: true }} grid={{ show: true }} tooltip={{ formatter: (datum) => { const accounts = datum.data?.accounts ?? datum.value; const adoptionRate = datum.data?.adoptionRate ?? datum.value / accounts; return [ datum.category, `Activated: ${formatAccounts(datum.value)}`, `Account base: ${accounts.toLocaleString()}`, `Adoption rate: ${(adoptionRate * 100).toFixed(1)}%`, ].join('\n'); }, }} /> ); }

Marketing Spend Multi Touch

Loading demo…

import { BarChart } from '@plocks/charts'; import { CAMPAIGN_SPEND, TOTAL_SPEND } from './data'; const formatSpend = (value: number) => `$${value.toLocaleString()}k`; export function Demo() { return ( <BarChart title="Marketing spend by channel" subtitle="Multi-touch journey campaign mix" h={420} data={CAMPAIGN_SPEND} barSpacing={0.28} legend={{ show: false }} valueFormatter={(value) => `${formatSpend(value)} invested`} valueLabel={{ color: '#1f2937', fontSize: 12, offset: 12, formatter: (value) => { const share = TOTAL_SPEND ? (value / TOTAL_SPEND) * 100 : 0; return `${share.toFixed(1)}% of spend`; }, }} yAxis={{ show: true, title: 'Investment (USD thousands)', titleFontSize: 12, labelFormatter: (value) => `$${value.toFixed(0)}k`, }} xAxis={{ show: true }} grid={{ show: true }} tooltip={{ formatter: (datum) => { const share = TOTAL_SPEND ? (datum.value / TOTAL_SPEND) * 100 : 0; return [ datum.category, `Spend: ${formatSpend(datum.value)}`, `Share: ${share.toFixed(1)}% of program`, datum.data?.objective ? `Objective: ${datum.data.objective}` : undefined, ] .filter(Boolean) .join('\n'); }, }} /> ); }

New Hire Recruiting Cycles

Loading demo…

import { BarChart } from '@plocks/charts'; import { RECRUITING_PROGRESS } from './data'; const formatDelta = (value: number, datum: (typeof RECRUITING_PROGRESS)[number]) => { const previous = datum.data?.previous ?? 0; const delta = value - previous; if (delta === 0) return 'No change vs last cycle'; const sign = delta > 0 ? '+' : '-'; return `${sign}${Math.abs(delta)} vs last cycle`; }; export function Demo() { return ( <BarChart title="New hires secured this recruiting cycle" subtitle="Compared with winter intake" h={440} orientation="horizontal" data={RECRUITING_PROGRESS} barSpacing={0.25} legend={{ show: false }} valueFormatter={(value) => `${value} hires`} valueLabel={{ formatter: (value, datum) => formatDelta(value, datum as (typeof RECRUITING_PROGRESS)[number]), color: '#1f2937', fontSize: 12, offset: 10, }} xAxis={{ title: 'Hires confirmed', labelFormatter: (value) => `${Math.round(value)}`, }} yAxis={{ show: true }} grid={{ show: true }} tooltip={{ formatter: (datum) => { const previous = datum.data?.previous ?? 0; const delta = datum.value - previous; const direction = delta >= 0 ? '+' : '-'; return [ `${datum.category}`, `This cycle: ${datum.value} hires`, `Last cycle: ${previous} hires`, `Delta: ${direction}${Math.abs(delta)}`, datum.data?.priority ? `Focus: ${datum.data.priority}` : undefined, datum.data?.openRoles != null ? `Open roles: ${datum.data.openRoles}` : undefined, ] .filter(Boolean) .join('\n'); }, }} /> ); }

Sla Compliance By Team

Loading demo…

import { BarChart } from '@plocks/charts'; import { SLA_COMPLIANCE } from './data'; const formatPercent = (value: number) => `${value.toFixed(1)}%`; export function Demo() { return ( <BarChart title="SLA compliance by response team" subtitle="Rolling 12-month attainment" h={420} orientation="horizontal" data={SLA_COMPLIANCE} barSpacing={0.3} legend={{ show: false }} valueFormatter={(value) => `${formatPercent(value)} SLA met`} valueLabel={{ color: '#111827', fontSize: 12, offset: 12, formatter: (value) => formatPercent(value), }} xAxis={{ title: 'Tickets meeting SLA target', labelFormatter: (value) => `${Math.round(value)}%`, }} yAxis={{ show: true }} grid={{ show: true }} tooltip={{ formatter: (datum) => { const last = datum.data?.lastQuarter ?? datum.value; const delta = datum.value - last; const direction = delta >= 0 ? '+' : '-'; return [ datum.category, `Current: ${formatPercent(datum.value)}`, `Last quarter: ${formatPercent(last)}`, `Change: ${direction}${Math.abs(delta).toFixed(1)} pts`, ].join('\n'); }, }} /> ); }
Scan to open this page on your phoneplocks.dev/charts/BarChart