Histogram Chart
Bin continuous values into buckets to inspect distributions, skew, and outliers.
Source
LLM docs
Package
npm install @plocks/chartsExamples
9
Properties
65
Playground
Basics
Bins session durations with a density curve. The bars take their color implicitly from the theme's first palette slot; no color props are set.
Loading demo…
import { HistogramChart } from '@plocks/charts';
import { SESSION_DURATIONS } from './data';
export function Demo() {
return (
<HistogramChart
title="Session duration distribution"
subtitle="Product analytics cohort"
h={280}
data={SESSION_DURATIONS}
bins={10}
showDensity
densityThickness={3}
densityColor="#12B886"
barGap={0.15}
tooltip={{
show: true,
formatter: (bin) => `${bin.count} sessions between ${bin.start}-${bin.end} min`,
}}
valueFormatter={(count, bin) => `${count} • ${bin.density.toFixed(2)} pdf`}
enableCrosshair
liveTooltip
/>
);
}Threshold coloring
Key settings
colorScale={{ type: 'threshold', thresholds: [2.5, 3.2] }}splits bins into three bands by where they sit on the x axis. A bin whose midpoint lands on a breakpoint takes the band above it.- Band colors are status colors (good, warning, critical), and
labelsgives each band a legend entry, so a band's meaning never rides on color alone.
Loading demo…
import { useState } from 'react';
import { View, Text } from 'react-native';
import { HistogramChart, HistogramBinSummary } from '@plocks/charts';
import { BREACH_LIMIT, LOAD_TIMES, SLO_TARGET } from './data';
export function Demo() {
const [focusedBin, setFocusedBin] = useState<HistogramBinSummary | null>(null);
return (
<View style={{ width: '100%', maxWidth: '100%' }}>
<HistogramChart
title="Page load time distribution"
subtitle="Bins colored by SLO status"
h={340}
data={LOAD_TIMES}
bins={14}
density={false}
showDensity={false}
barOpacity={0.9}
colorScale={{
type: 'threshold',
by: 'x',
thresholds: [SLO_TARGET, BREACH_LIMIT],
colors: ['#0ca30c', '#fab219', '#d03b3b'],
labels: ['Within SLO', 'At risk', 'Breaching'],
}}
legend={{ show: true }}
annotations={[
{
id: 'slo-target',
shape: 'vertical-line',
x: SLO_TARGET,
color: '#71717A',
label: 'SLO 2.5s',
},
]}
xAxis={{
title: 'Page load time (seconds)',
labelFormatter: (value) => `${value.toFixed(1)}s`,
}}
yAxis={{
title: 'Page views',
}}
grid={{ show: true }}
tooltip={{
show: true,
formatter: (bin) => `${bin.count} page views between ${bin.start.toFixed(1)}–${bin.end.toFixed(1)}s`,
}}
valueFormatter={(count) => `${count} views`}
onBinFocus={(summary) => setFocusedBin(summary)}
onBinBlur={() => setFocusedBin(null)}
/>
<View style={{ paddingHorizontal: 4, marginTop: 12 }}>
{focusedBin ? (
<Text style={{ fontSize: 13, color: '#3F3F46' }}>
{`${focusedBin.count} loads between ${focusedBin.start.toFixed(2)}–${focusedBin.end.toFixed(2)}s · percentile ${(focusedBin.percentile * 100).toFixed(1)}% · cumulative ${(focusedBin.cumulativeDensityRatio * 100).toFixed(1)}% density`}
</Text>
) : (
<Text style={{ fontSize: 13, color: '#52525B' }}>
Hover a bar to highlight its percentile and cumulative share of traffic.
</Text>
)}
</View>
</View>
);
}Diverging from a target
Key settings
colorScale={{ type: 'diverging', midpoint: 3.9, colors: [low, high] }}colors bins by which side of the target they fall on. A neutral gray middle is added automatically.The arms are symmetric: bins the same distance from the midpoint get the same intensity, even though the data reaches further below the target than above it.
Loading demo…
import { HistogramChart } from '@plocks/charts';
import { BATTERY_VOLTAGES, REPLACEMENT_THRESHOLD, TARGET_VOLTAGE } from './data';
export function Demo() {
return (
<HistogramChart
title="Sensor battery voltage after firmware upgrade"
subtitle="Diverging from the 3.9V target: red runs low, blue runs high"
h={320}
data={BATTERY_VOLTAGES}
bins={12}
binMethod="sturges"
density={false}
showDensity={false}
barOpacity={0.9}
colorScale={{
type: 'diverging',
midpoint: TARGET_VOLTAGE,
colors: ['#e34948', '#2a78d6'],
}}
annotations={[
{
id: 'replacement-line',
shape: 'vertical-line',
x: REPLACEMENT_THRESHOLD,
color: '#e34948',
label: 'Replace below 3.5V',
},
{
id: 'target-line',
shape: 'vertical-line',
x: TARGET_VOLTAGE,
color: '#71717A',
label: 'Target 3.9V',
},
]}
xAxis={{
title: 'Voltage (V)',
labelFormatter: (value) => `${value.toFixed(2)}V`,
}}
yAxis={{
title: 'Sensors',
}}
grid={{ show: true }}
tooltip={{
show: true,
formatter: (bin) => `${bin.count} sensors between ${bin.start.toFixed(2)}–${bin.end.toFixed(2)}V`,
}}
valueFormatter={(count) => `${count} sensors`}
/>
);
}Sequential shading by count
Key settings
colorScale={{ type: 'sequential', by: 'count' }}shades each bin by how many people it holds instead of by where it sits (by: 'x', the default).- With no
colors, the ramp is built from the bar color itself: a tint that recedes toward the chart surface at the low end, a deeper shade at the high end. On a dark theme the same rule flips the anchor automatically.
Loading demo…
import { HistogramChart } from '@plocks/charts';
import { TENURE_YEARS, medianTenure } from './data';
export function Demo() {
return (
<HistogramChart
title="Employee tenure distribution"
subtitle="Shaded by headcount: bolder bins hold more people"
h={320}
data={TENURE_YEARS}
bins={12}
binMethod="sqrt"
showDensity
densityThickness={2.5}
barOpacity={0.9}
colorScale={{ type: 'sequential', by: 'count' }}
annotations={[
{
id: 'median-tenure',
shape: 'vertical-line',
x: Number(medianTenure.toFixed(2)),
color: '#71717A',
label: `Median ${medianTenure.toFixed(1)} yrs`,
},
]}
xAxis={{
title: 'Tenure (years)',
labelFormatter: (value) => `${value.toFixed(1)} yrs`,
}}
yAxis={{
title: 'Probability density',
labelFormatter: (value) => value.toFixed(2),
}}
grid={{ show: true }}
tooltip={{
show: true,
formatter: (bin) => `${bin.count} teammates between ${bin.start.toFixed(1)}–${bin.end.toFixed(1)} years`,
}}
valueFormatter={(count, bin) => `${count} people · pdf ${bin.density.toFixed(3)}`}
/>
);
}Gradient fill
Key settings
barColoraccepts a gradient as well as a color:{ angle, stops, extent }.angleis in degrees; 90 runs top to bottom.extent: 'plot'lays one gradient across the whole plot, so each bar shows the slice behind it and taller bins reach the deeper end. The default,extent: 'mark', gives every bar the full gradient.
Loading demo…
import { HistogramChart } from '@plocks/charts';
import { DELIVERY_MINUTES } from './data';
export function Demo() {
return (
<HistogramChart
title="Delivery time distribution"
subtitle="One gradient spans the plot, so taller bins reach the deeper end"
h={320}
data={DELIVERY_MINUTES}
bins={14}
density={false}
showDensity={false}
barOpacity={1}
barRadius={4}
barColor={{
angle: 90,
extent: 'plot',
stops: [
{ offset: 0, color: '#1c5cab' },
{ offset: 1, color: '#86b6ef' },
],
}}
xAxis={{
title: 'Minutes from order to door',
labelFormatter: (value) => `${value.toFixed(0)}m`,
}}
yAxis={{
title: 'Orders',
}}
grid={{ show: true }}
tooltip={{
show: true,
formatter: (bin) => `${bin.count} orders in ${bin.start.toFixed(0)}–${bin.end.toFixed(0)} min`,
}}
valueFormatter={(count) => `${count} orders`}
/>
);
}Outlined bars
Key settings
- A single explicit
barColoratbarOpacity={0.18}, outlined withbarStrokeandbarStrokeWidth. The density line uses the same accent, so the chart reads as one series.
Loading demo…
import { HistogramChart } from '@plocks/charts';
import { RESPONSE_HOURS } from './data';
const ACCENT = '#199e70';
export function Demo() {
return (
<HistogramChart
title="Support first-response time"
subtitle="Outlined bars: a faint fill under a solid stroke"
h={320}
data={RESPONSE_HOURS}
bins={12}
barColor={ACCENT}
barOpacity={0.18}
barStroke={ACCENT}
barStrokeWidth={1.5}
barRadius={6}
barGap={0.16}
showDensity
densityColor={ACCENT}
densityThickness={2}
xAxis={{
title: 'Hours to first response',
labelFormatter: (value) => `${value.toFixed(0)}h`,
}}
yAxis={{
title: 'Probability density',
labelFormatter: (value) => value.toFixed(2),
}}
grid={{ show: true }}
tooltip={{
show: true,
formatter: (bin) => `${bin.count} tickets answered in ${bin.start.toFixed(1)}–${bin.end.toFixed(1)}h`,
}}
valueFormatter={(count) => `${count} tickets`}
/>
);
}Themed with a brand palette
Key settings
- The chart sets no color props. Bars take palette slot 1 and the density line takes slot 2 from the nearest
ChartThemeProvider. - A nested provider re-themes only its own subtree: it inherits text, grid and surface colors from the app's theme and overrides just
accentPalette.
Loading demo…
import { ChartThemeProvider, HistogramChart } from '@plocks/charts';
import { BASKET_TOTALS, BRAND_PALETTE } from './data';
export function Demo() {
// No color props on the chart: bars take palette slot 1 and the density line
// slot 2 from the nearest ChartThemeProvider. A nested provider re-themes just
// its own subtree and inherits everything else from the app's theme.
return (
<ChartThemeProvider value={{ colors: { accentPalette: BRAND_PALETTE } }}>
<HistogramChart
title="Checkout basket size"
subtitle="Colored entirely by the surrounding theme's palette"
h={320}
data={BASKET_TOTALS}
bins={12}
showDensity
legend={{ show: true }}
xAxis={{
title: 'Basket total (USD)',
labelFormatter: (value) => `$${value.toFixed(0)}`,
}}
yAxis={{
title: 'Probability density',
labelFormatter: (value) => value.toFixed(3),
}}
grid={{ show: true }}
tooltip={{
show: true,
formatter: (bin) => `${bin.count} baskets between $${bin.start.toFixed(0)}–$${bin.end.toFixed(0)}`,
}}
valueFormatter={(count) => `${count} baskets`}
/>
</ChartThemeProvider>
);
}Contract Length Retention
Loading demo…
import { HistogramChart } from '@plocks/charts';
import { CONTRACT_LENGTHS, median } from './data';
export function Demo() {
return (
<HistogramChart
title="Customer contract length distribution"
subtitle="Used to calibrate retention and renewal strategy"
h={320}
data={CONTRACT_LENGTHS}
bins={12}
binMethod="sturges"
density={false}
showDensity={false}
barOpacity={0.82}
rangeHighlights={[{ id: 'core-subscription', start: 12, end: 24, color: '#22C55E', opacity: 0.14 }]}
annotations={[
{
id: 'one-year',
shape: 'vertical-line',
x: 12,
color: '#22C55E',
label: '1 year',
},
{
id: 'two-year',
shape: 'vertical-line',
x: 24,
color: '#15803D',
label: '2 years',
},
{
id: 'median',
shape: 'vertical-line',
x: median,
color: '#F97316',
label: `Median ${median} mo`,
},
]}
xAxis={{
title: 'Contract length (months)',
}}
yAxis={{
title: 'Customer accounts',
labelFormatter: (value) => `${value.toFixed(0)}`,
}}
grid={{ show: true }}
tooltip={{
show: true,
formatter: (bin) => `${bin.count} accounts between ${bin.start.toFixed(0)}–${bin.end.toFixed(0)} months`,
}}
valueFormatter={(count) => `${count} customers`}
/>
);
}Transaction Amount Fraud
Loading demo…
import { HistogramChart } from '@plocks/charts';
import { REVIEW_THRESHOLD, TRANSACTION_AMOUNTS } from './data';
export function Demo() {
return (
<HistogramChart
title="Transaction amount distribution"
subtitle="Identifying anomalous high-value purchases"
h={320}
data={TRANSACTION_AMOUNTS}
bins={16}
binMethod="fd"
showDensity
barOpacity={0.76}
densityColor="#0EA5E9"
rangeHighlights={[
{ id: 'high-risk-window', start: 900, end: 1400, color: '#EF4444', opacity: 0.12 },
]}
annotations={[
{
id: 'manual-review',
shape: 'vertical-line',
x: REVIEW_THRESHOLD,
color: '#DC2626',
label: 'Manual review starts',
},
]}
xAxis={{
title: 'Transaction amount (USD)',
labelFormatter: (value) => `$${value.toFixed(0)}`,
}}
yAxis={{
title: 'Probability density',
labelFormatter: (value) => value.toFixed(3),
}}
grid={{ show: true }}
tooltip={{
show: true,
formatter: (bin) => `${bin.count} orders between $${bin.start.toFixed(0)}–$${bin.end.toFixed(0)}`,
}}
valueFormatter={(count, bin) => `${count} orders · pdf ${bin.density.toFixed(3)}`}
/>
);
}Related charts
Further reading
React Native design for iOS, Android, and Web.
Quick Links
Resources