Histogram Chart

Bin continuous values into buckets to inspect distributions, skew, and outliers.

npm install @plocks/charts

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 labels gives 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
  • barColor accepts a gradient as well as a color: { angle, stops, extent }. angle is 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 barColor at barOpacity={0.18}, outlined with barStroke and barStrokeWidth.
  • 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)}`} /> ); }
Scan to open this page on your phoneplocks.dev/charts/HistogramChart

Related charts