Skip to main content
BarChart
@coinbase/cds-web-visualization@3.4.0-beta.1
Import
import { BarChart } from '@coinbase/cds-web-visualization'
SourceView source code
Related components

Basic Example

Bar charts are a useful component for comparing discrete categories of data. They are helpful for highlighting trends to users or allowing them to compare proportions at a glance.

To start, pass in a series of data to the chart.

Loading...
Live Code
<BarChart
  height={250}
  series={[
    {
      id: 'prices',
      data: [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58],
    },
  ]}
  curve="monotone"
  showYAxis
  yAxis={{
    showGrid: true,
  }}
/>

Multiple Series

You can also provide multiple series of data to the chart. Series will have their bars for each data point rendered side by side.

Loading...
Live Code
function MonthlyGainsByAsset() {
  const ThinSolidLine = memo((props: SolidLineProps) => <SolidLine {...props} strokeWidth={1} />);

  const tickFormatter = useCallback(
    (amount) =>
      new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency: 'USD',
      }).format(amount),
    [],
  );

  return (
    <BarChart
      height={400}
      series={[
        {
          id: 'btc',
          data: [
            345.82, 510.63, 280.19, 720.5, 655.37, 410.23, 580.96, 815.44, 740.18, 910.71, 975.02,
            620.57,
          ],
          color: assets.btc.color,
        },
        {
          id: 'eth',
          data: [
            270.49, 425.21, 190.75, 680.91, 610.88, 350.67, 440.11, 780.08, 705.83, 840.26, 920.65,
            550.93,
          ],
          color: assets.eth.color,
        },
      ]}
      showYAxis
      yAxis={{
        showGrid: true,
        GridLineComponent: ThinSolidLine,
        tickLabelFormatter: tickFormatter,
        width: 62,
      }}
    />
  );
}

Series Stacking

You can also configure stacking for your chart using the stacked prop.

Loading...
Live Code
function MonthlyGainsByAsset() {
  const ThinSolidLine = memo((props: SolidLineProps) => <SolidLine {...props} strokeWidth={1} />);

  const tickFormatter = useCallback(
    (amount) =>
      new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency: 'USD',
      }).format(amount),
    [],
  );

  return (
    <BarChart
      height={400}
      series={[
        {
          id: 'xrp',
          data: [
            170.22, 340.34, 305.98, 225.39, 250.76, 130.53, 115.81, 195.04, 210.28, 60.42, 120.94,
            85.12,
          ],
          color: assets.xrp.color,
        },
        {
          id: 'ltc',
          data: [
            450.77, 615.33, 355.68, 880.15, 810.99, 520.46, 590.29, 960.52, 910.07, 1020.41, 851.89,
            750.61,
          ],
          color: assets.ltc.color,
        },
        {
          id: 'eth',
          data: [
            270.49, 425.21, 190.75, 680.91, 610.88, 350.67, 440.11, 780.08, 705.83, 840.26, 920.65,
            550.93,
          ],
          color: assets.eth.color,
        },
        {
          id: 'btc',
          data: [
            345.82, 510.63, 280.19, 720.5, 655.37, 410.23, 580.96, 815.44, 740.18, 910.71, 975.02,
            620.57,
          ],
          color: assets.btc.color,
        },
      ]}
      stacked
      showYAxis
      yAxis={{
        showGrid: true,
        GridLineComponent: ThinSolidLine,
        tickLabelFormatter: tickFormatter,
        width: 62,
      }}
    />
  );
}

You can also configure multiple stacks by setting the stackId prop on each series.

Loading...
Live Code
function MonthlyGainsMultipleStacks() {
  const ThinSolidLine = memo((props: SolidLineProps) => <SolidLine {...props} strokeWidth={1} />);

  const tickFormatter = useCallback(
    (amount) =>
      new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency: 'USD',
      }).format(amount),
    [],
  );

  return (
    <BarChart
      height={400}
      series={[
        {
          id: 'xrp',
          data: [
            170.22, 340.34, 305.98, 225.39, 250.76, 130.53, 115.81, 195.04, 210.28, 60.42, 120.94,
            85.12,
          ],
          color: assets.xrp.color,
          stackId: 'shortTerm',
        },
        {
          id: 'ltc',
          data: [
            450.77, 615.33, 355.68, 880.15, 810.99, 520.46, 590.29, 960.52, 910.07, 1020.41, 851.89,
            750.61,
          ],
          color: assets.ltc.color,
          stackId: 'shortTerm',
        },
        {
          id: 'eth',
          data: [
            270.49, 425.21, 190.75, 680.91, 610.88, 350.67, 440.11, 780.08, 705.83, 840.26, 920.65,
            550.93,
          ],
          color: assets.eth.color,
          stackId: 'longTerm',
        },
        {
          id: 'btc',
          data: [
            345.82, 510.63, 280.19, 720.5, 655.37, 410.23, 580.96, 815.44, 740.18, 910.71, 975.02,
            620.57,
          ],
          color: assets.btc.color,
          stackId: 'longTerm',
        },
      ]}
      stacked
      showYAxis
      yAxis={{
        showGrid: true,
        GridLineComponent: ThinSolidLine,
        tickLabelFormatter: tickFormatter,
        width: 62,
      }}
    />
  );
}

Stack Gap

Loading...
Live Code
function MonthlyGainsByAsset() {
  const ThinSolidLine = memo((props: SolidLineProps) => <SolidLine {...props} strokeWidth={1} />);

  const tickFormatter = useCallback(
    (amount) =>
      new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency: 'USD',
      }).format(amount),
    [],
  );

  return (
    <BarChart
      height={400}
      series={[
        {
          id: 'xrp',
          data: [
            170.22, 340.34, 305.98, 225.39, 250.76, 130.53, 115.81, 195.04, 210.28, 60.42, 120.94,
            500,
          ],
          color: assets.xrp.color,
        },
        {
          id: 'ltc',
          data: [
            450.77, 615.33, 355.68, 880.15, 810.99, 520.46, 590.29, 960.52, 910.07, 1020.41, 851.89,
            500,
          ],
          color: assets.ltc.color,
        },
        {
          id: 'eth',
          data: [
            270.49, 425.21, 190.75, 680.91, 610.88, 350.67, 440.11, 780.08, 705.83, 840.26, 920.65,
            500,
          ],
          color: assets.eth.color,
        },
        {
          id: 'btc',
          data: [
            345.82, 510.63, 280.19, 720.5, 655.37, 410.23, 580.96, 815.44, 740.18, 910.71, 975.02,
            500,
          ],
          color: assets.btc.color,
        },
      ]}
      stackGap={4}
      stacked
      showYAxis
      yAxis={{
        showGrid: true,
        GridLineComponent: ThinSolidLine,
        tickLabelFormatter: tickFormatter,
        width: 62,
        domainLimit: 'strict'
      }}
    />
  );
}

Border Radius

Bars have a default border radius of 100. You can change this by setting the borderRadius prop on the chart.

Stacks will only round the top corners of touching bars.

Loading...
Live Code
<BarChart
  stacked
  borderRadius={1000}
  height={300}
  maxWidth={400}
  padding={0}
  series={[
    { id: 'purple', data: [null, 6, 8, 10, 7, 6, 6, 8, 9, 12, 10, 4], color: '#b399ff' },
    { id: 'blue', data: [null, 10, 12, 11, 10, 9, 10, 11, 7, 4, 12, 18], color: '#4f7cff' },
    { id: 'cyan', data: [null, 7, 10, 12, 11, 10, 8, 11, 5, 12, 2, 9], color: '#00c2df' },
    {
      id: 'green',
      data: [10, null, null, null, 1, null, null, 6, null, null, null, null],
      color: '#33c481',
    },
  ]}
  showXAxis
  xAxis={{
    data: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
    tickLabelFormatter: (value) => {
      if (value === 'D') {
        return <tspan style={{ fontWeight: 'bold' }}>{value}</tspan>;
      }
      return value;
    },
  }}
  style={{ margin: '0 auto' }}
/>

Round Baseline

You can also round the baseline of the bars by setting the roundBaseline prop on the chart.

Loading...
Live Code
<BarChart
  roundBaseline
  stacked
  borderRadius={1000}
  height={300}
  maxWidth={400}
  padding={0}
  series={[
    { id: 'purple', data: [null, 6, 8, 10, 7, 6, 6, 8, 9, 12, 10, 4], color: '#b399ff' },
    { id: 'blue', data: [null, 10, 12, 11, 10, 9, 10, 11, 7, 4, 12, 18], color: '#4f7cff' },
    { id: 'cyan', data: [null, 7, 10, 12, 11, 10, 8, 11, 5, 12, 2, 9], color: '#00c2df' },
    {
      id: 'green',
      data: [10, null, null, null, 1, null, null, 6, null, null, null, null],
      color: '#33c481',
    },
  ]}
  showXAxis
  xAxis={{
    data: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
    tickLabelFormatter: (value) => {
      if (value === 'D') {
        return <tspan style={{ fontWeight: 'bold' }}>{value}</tspan>;
      }
      return value;
    },
  }}
  style={{ margin: '0 auto' }}
/>

Negative Data

Loading...
Live Code
function PositiveAndNegativeCashFlow() {
  const ThinSolidLine = memo((props: SolidLineProps) => <SolidLine {...props} strokeWidth={1} />);

  const categories = Array.from({ length: 31 }, (_, i) => `3/${i + 1}`);
  const gains = [
    5, 0, 6, 18, 0, 5, 12, 0, 12, 22, 28, 18, 0, 12, 6, 0, 0, 24, 0, 0, 4, 0, 18, 0, 0, 14, 10, 16,
    0, 0, 0,
  ];

  const losses = [
    -4, 0, -8, -12, -6, 0, 0, 0, -18, 0, -12, 0, -9, -6, 0, 0, 0, 0, -22, -8, 0, 0, -10, -14, 0, 0,
    0, 0, 0, -12, -10,
  ];
  const series = [
    { id: 'gains', data: gains, color: 'var(--color-fgPositive)' },
    { id: 'losses', data: losses, color: 'var(--color-fgNegative)' },
  ];

  return (
    <BarChart
      height={420}
      padding={4}
      series={series}
      xAxis={{ data: categories }}
      stacked
      showXAxis
      showYAxis
      yAxis={{
        showGrid: true,
        GridLineComponent: ThinSolidLine,
        tickLabelFormatter: (value) => `$${value}M`,
      }}
    />
  );
}

Missing Bars

You can pass in null or 0 values to not render a bar for that data point.

Loading...
Live Code
<BarChart
  showXAxis
  showYAxis
  height={400}
  series={[
    {
      id: 'weekly-data',
      data: [45, null, 38, 0, 19, null, 32],
    },
  ]}
  xAxis={{
    data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
    showTickMarks: true,
    showLine: true,
  }}
  yAxis={{
    requestedTickCount: 5,
    tickLabelFormatter: (value) => `$${value}k`,
    showGrid: true,
    showTickMarks: true,
    showLine: true,
    tickMarkSize: 1.5,
    domain: { max: 50 },
  }}
/>

You can also use the BarStackComponent prop to render an empty circle for zero values.

Loading...
Live Code
function MonthlyRewards() {
  const months = ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'];
  const currentMonth = 7;
  const purple = [null, 6, 8, 10, 7, 6, 6, 8, null, null, null, null];
  const blue = [null, 10, 12, 11, 10, 9, 10, 11, null, null, null, null];
  const cyan = [null, 7, 10, 12, 11, 10, 8, 11, null, null, null, null];
  const green = [10, null, null, null, 1, null, null, 6, null, null, null, null];

  const series = [
    { id: 'purple', data: purple, color: '#b399ff' },
    { id: 'blue', data: blue, color: '#4f7cff' },
    { id: 'cyan', data: cyan, color: '#00c2df' },
    { id: 'green', data: green, color: '#33c481' },
  ];

  const CustomBarStackComponent = ({ children, ...props }: BarStackComponentProps) => {
    if (props.height === 0) {
      const diameter = props.width;
      return (
        <Bar
          roundBottom
          roundTop
          borderRadius={1000}
          fill="var(--color-bgTertiary)"
          height={diameter}
          width={diameter}
          x={props.x}
          y={props.y - diameter}
        />
      );
    }

    return <DefaultBarStack {...props}>{children}</DefaultBarStack>;
  };

  return (
    <BarChart
      roundBaseline
      showXAxis
      stacked
      BarStackComponent={CustomBarStackComponent}
      borderRadius={1000}
      height={300}
      padding={0}
      series={series}
      showYAxis={false}
      stackMinSize={3}
      width={403}
      xAxis={{
        tickLabelFormatter: (index) => {
          if (index == currentMonth) {
            return <tspan style={{ fontWeight: 'bold' }}>{months[index]}</tspan>;
          }
          return months[index];
        },
        categoryPadding: 0.27,
      }}
    />
  );
};

Customization

Bar Spacing

There are two ways to control the spacing between bars. You can set the barPadding prop to control the spacing between bars within a series. You can also set the categoryPadding prop to control the spacing between stacks of bars.

Loading...
Live Code
<BarChart
  height={400}
  series={[
    {
      id: 'pageViews',
      data: [24, 13, 98, 39, 48, 38, 43],
      color: 'var(--color-fgPositive)',
    },
    {
      id: 'uniqueVisitors',
      data: [12, 15, 18, 21, 24, 27, 30],
      color: 'var(--color-fgNegative)',
    },
  ]}
  borderRadius={0}
  barPadding={0}
  showYAxis
  yAxis={{
    showGrid: true,
  }}
  xAxis={{
    categoryPadding: 0.2,
  }}
/>

Minimum Size

To better emphasize small values, you can set the stackMinSize or barMinSize prop to control the minimum size for entire stacks or individual bar. It is recommended to only use stackMinSize for stacked charts and barMinSize for non-stacked charts.

Minimum Stack Size

You can set the stackMinSize prop to control the minimum size for entire stacks. This will only apply to stacks that have a value that is not null or 0. It will proportionally scale the values of each bar in the stack to reach the minimum size.

Loading...
Live Code
<BarChart
  height={400}
  series={[
    {
      id: 'pageViews',
      data: [24, 3, 98, null, 48, null, 43],
      color: 'var(--color-fgPositive)',
    },
    {
      id: 'uniqueVisitors',
      data: [12, 1, 18, null, 24, 1, 30],
      color: 'var(--color-fgNegative)',
    },
  ]}
  stackMinSize={2}
  stacked
  showYAxis
  yAxis={{
    showGrid: true,
  }}
/>

Minimum Bar Size

You can also set the barMinSize prop to control the minimum size for individual bars. This will only apply to bars that have a value that is not null or 0.

Loading...
Live Code
<BarChart
  showXAxis
  showYAxis
  height={400}
  series={[
    {
      id: 'weekly-data',
      data: [45, 52, 0, 45, null, 1, 32],
    },
  ]}
  xAxis={{
    data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
    showTickMarks: true,
    showLine: true,
  }}
  yAxis={{
    requestedTickCount: 5,
    tickLabelFormatter: (value) => `$${value}k`,
    showGrid: true,
    showTickMarks: true,
    showLine: true,
    tickMarkSize: 1.5,
    domain: { max: 50 },
  }}
  barMinSize={4}
/>

Multiple Y Axes

You can render bars from separate y axes in one BarPlot, however they aren't able to be stack.

Loading...
Live Code
<VStack gap={2}>
  <CartesianChart
    height={400}
    series={[
      {
        id: 'revenue',
        data: [455, 520, 380, 455, 285, 235],
        yAxisId: 'revenue',
        color: 'var(--color-accentBoldYellow)',
      },
      {
        id: 'profitMargin',
        data: [23, 20, 16, 38, 12, 9],
        yAxisId: 'profitMargin',
        color: 'var(--color-fgPositive)',
      },
    ]}
    xAxis={{
      data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
      scaleType: 'band',
    }}
    yAxis={[
      {
        id: 'revenue',
        domain: { min: 0 },
      },
      {
        id: 'profitMargin',
        domain: { min: 0, max: 100 },
      },
    ]}
  >
    <XAxis showLine showTickMarks />
    <YAxis
      showGrid
      showLine
      showTickMarks
      axisId="revenue"
      position="left"
      requestedTickCount={5}
      width={60}
      tickLabelFormatter={(value) => `$${value}k`}
    />
    <YAxis
      showLine
      showTickMarks
      axisId="profitMargin"
      position="right"
      requestedTickCount={5}
      tickLabelFormatter={(value) => `${value}%`}
    />
    <BarPlot />
  </CartesianChart>
  <HStack justifyContent="center" gap={2}>
    <Box alignItems="center" gap={0.5}>
      <Box
        borderRadius={1000}
        width={10}
        height={10}
        style={{ background: 'var(--color-accentBoldYellow)' }}
      />
      <Text font="label2">Revenue ($)</Text>
    </Box>
    <Box alignItems="center" gap={0.5}>
      <Box
        borderRadius={1000}
        width={10}
        height={10}
        style={{ background: 'var(--color-fgPositive)' }}
      />
      <Text font="label2">Profit Margin (%)</Text>
    </Box>
  </HStack>
</VStack>

Custom Components

Candlesticks

You can set the BarComponent prop to render a custom component for bars.

Loading...
Live Code
function Candlesticks() {
  const infoTextId = useId();
  const infoTextRef = React.useRef<HTMLSpanElement>(null);
  const selectedIndexRef = React.useRef<number | null>(null);
  const stockData = btcCandles.slice(0, 90)
    .reverse();
  const min = Math.min(...stockData.map((data) => parseFloat(data.low)));

  const ThinSolidLine = memo((props: SolidLineProps) => <SolidLine {...props} strokeWidth={1} />);

  const candlesData = stockData.map((data) => [parseFloat(data.low), parseFloat(data.high)]) as [
    number,
    number,
  ][];

  const CandlestickBarComponent = memo<BarComponentProps>(
    ({ x, y, width, height, originY, dataX, ...props }) => {
      const { getYScale } = useCartesianChartContext();
      const yScale = getYScale();

      const wickX = x + width / 2;

      const timePeriodValue = stockData[dataX as number];

      const open = parseFloat(timePeriodValue.open);
      const close = parseFloat(timePeriodValue.close);

      const bullish = open < close;
      const color = bullish ? 'var(--color-fgPositive)' : 'var(--color-fgNegative)';
      const openY = yScale?.(open) ?? 0;
      const closeY = yScale?.(close) ?? 0;

      const bodyHeight = Math.abs(openY - closeY);
      const bodyY = openY < closeY ? openY : closeY;

      return (
        <g>
          <line stroke={color} strokeWidth={1} x1={wickX} x2={wickX} y1={y} y2={y + height} />
          <rect fill={color} height={bodyHeight} width={width} x={x} y={bodyY} />
        </g>
      );
    },
  );

  const formatPrice = React.useCallback((price: string) => {
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'USD',
    }).format(parseFloat(price));
  }, []);

  const formatVolume = React.useCallback((volume: string) => {
    const volumeInThousands = parseFloat(volume) / 1000;
    return (
      new Intl.NumberFormat('en-US', {
        style: 'decimal',
        minimumFractionDigits: 0,
        maximumFractionDigits: 2,
      }).format(volumeInThousands) + 'k'
    );
  }, []);

  const formatTime = React.useCallback(
    (index) => {
      if (index === null || index === undefined || index >= stockData.length) return '';
      const ts = parseInt(stockData[index].start);
      return new Date(ts * 1000).toLocaleDateString('en-US', {
        month: 'short',
        day: 'numeric',
      });
    },
    [stockData],
  );

  const updateInfoText = React.useCallback(
    (index) => {
      if (!infoTextRef.current) return;

      const text =
        index !== null && index !== undefined
          ? `Open: ${formatPrice(stockData[index].open)}, Close: ${formatPrice(
              stockData[index].close,
            )}, Volume: ${formatVolume(stockData[index].volume)}`
          : formatPrice(stockData[stockData.length - 1].close);

      infoTextRef.current.textContent = text;
      selectedIndexRef.current = index;
    },
    [stockData, formatPrice, formatVolume],
  );
  const initialInfo = formatPrice(stockData[stockData.length - 1].close);

  React.useEffect(() => {
    updateInfoText(selectedIndexRef.current);
  }, [stockData, updateInfoText]);

  return (
    <VStack gap={2}>
      <Text font="headline" id={infoTextId} aria-live="polite">
        <span ref={infoTextRef}>{initialInfo}</span>
      </Text>
      <BarChart
        enableScrubbing
        showXAxis
        showYAxis
        BarComponent={CandlestickBarComponent}
        BarStackComponent={({ children }) => <g>{children}</g>}
        animate={false}
        borderRadius={0}
        height={400}
        onScrubberPositionChange={updateInfoText}
        series={[
          {
            id: 'stock-prices',
            data: candlesData,
          },
        ]}
        xAxis={{
          tickLabelFormatter: formatTime,
        }}
        yAxis={{
          domain: { min },
          tickLabelFormatter: formatPrice,
          width: 80,
          showGrid: true,
          GridLineComponent: ThinSolidLine,
        }}
        aria-labelledby={infoTextId}
      >
        <Scrubber
            hideOverlay
            LineComponent={(props) => (
              <ReferenceLine {...props} LineComponent={ThinSolidLine} />
            )}
            seriesIds={[]}
          />
      </BarChart>
    </VStack>
  );
};

Outlined Stacks

You can set the BarStackComponent prop to render a custom component for stacks.

Loading...
Live Code
function MonthlyRewards() {
  const CustomBarStackComponent = ({ children, ...props }: BarStackComponentProps) => {
    return (
      <>
        <Bar
          roundBottom
          roundTop
          borderRadius={1000}
          stroke="var(--color-fg)"
          strokeWidth={4}
          height={props.height}
          width={props.width}
          x={props.x}
          y={props.y}
          clip
        />
        <DefaultBarStack {...props}>{children}</DefaultBarStack>
      </>
    );
  };

  return (
    <BarChart
      roundBaseline
      stacked
      BarStackComponent={CustomBarStackComponent}
      borderRadius={1000}
      height={300}
      maxWidth={400}
      padding={0}
      series={[
        { id: 'purple', data: [null, 6, 8, 10, 7, 6, 6, 8, 9, 12, 10, 4], color: '#b399ff' },
        { id: 'blue', data: [null, 10, 12, 11, 10, 9, 10, 11, 7, 4, 12, 18], color: '#4f7cff' },
        { id: 'cyan', data: [null, 7, 10, 12, 11, 10, 8, 11, 5, 12, 2, 9], color: '#00c2df' },
        {
          id: 'green',
          data: [10, null, null, null, 1, null, null, 6, null, null, null, null],
          color: '#33c481',
        },
      ]}
      showXAxis
      xAxis={{
        data: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
        tickLabelFormatter: (value) => {
          if (value === 'D') {
            return <tspan style={{ fontWeight: 'bold' }}>{value}</tspan>;
          }
          return value;
        },
      }}
      yAxis={{ range: ({ min, max }) => ({ min, max: max - 4 }) }}
      style={{ margin: '0 auto' }}
    />
  );
}

Is this page useful?

Coinbase Design is an open-source, adaptable system of guidelines, components, and tools that aid the best practices of user interface design for crypto products.