How To Make Bar Chart with React

May 22, 2023

3 min read

How To Make Bar Chart with React
Watch on YouTube

While there are plenty of libraries for data visualization, it often might be easier to implement a custom component that fits your exact needs and UI requirements. Let me share how I've implemented a simple Bar Chart so you can find some useful insights for your task.

example

The component receives a list of items or bars, the exact height of the BarChart, and two optional parameters for expected height for values we show under and above the bar. We are less likely to specify them, except if we want to make labels or values larger or smaller.

import { toPercents } from "lib/shared/utils/toPercents"
import { ReactNode } from "react"
import styled from "styled-components"
import { SameWidthChildrenRow } from "../Layout/SameWidthChildrenRow"
import { Spacer } from "../Spacer"
import { VStack } from "../Stack"
import { defaultTransitionCSS } from "../animations/transitions"
import { HSLA } from "../colors/HSLA"
import { centerContentCSS } from "../utils/centerContentCSS"
import { getCSSUnit } from "../utils/getCSSUnit"
import { Text } from "../Text"

export interface BarChartItem {
  label: ReactNode
  value: number
  color: HSLA
  renderValue?: (value: number) => ReactNode
}

interface BarChartProps {
  items: BarChartItem[]
  height: React.CSSProperties["height"]
  expectedValueHeight?: React.CSSProperties["height"]
  expectedLabelHeight?: React.CSSProperties["height"]
}

const barValueGap = "4px"
const barLabelGap = "4px"
const defaultLabelSize = 12

const Bar = styled.div`
  border-radius: 4px;
  ${defaultTransitionCSS};
`

const RelativeWrapper = styled.div`
  position: relative;
  ${centerContentCSS};
`

export const BarPlaceholder = styled(Bar)`
  height: 2px;
  background: ${({ theme }) => theme.colors.backgroundGlass.toCssValue()};
`

const Value = styled(Text)`
  position: absolute;
  white-space: nowrap;
  line-height: 1;
  bottom: ${barValueGap};
  color: ${({ theme }) => theme.colors.textSupporting.toCssValue()};
`

const Label = styled(Value)`
  top: ${barLabelGap};
`

const Content = styled(SameWidthChildrenRow)`
  flex: 1;
`

const Column = styled(VStack)`
  height: 100%;
  justify-content: end;
`

export const BarChart = ({
  items,
  height,
  expectedValueHeight = defaultLabelSize,
  expectedLabelHeight = defaultLabelSize,
}: BarChartProps) => {
  const maxValue = Math.max(...items.map((item) => item.value))

  return (
    <VStack style={{ height }}>
      <Spacer
        height={`calc(${getCSSUnit(expectedValueHeight)} + ${barValueGap})`}
      />
      <Content gap={4}>
        {items.map(({ value, color, renderValue, label }, index) => {
          return (
            <Column key={index}>
              {renderValue && (
                <RelativeWrapper>
                  <Value style={{ fontSize: defaultLabelSize }} as="div">
                    {renderValue(value)}
                  </Value>
                </RelativeWrapper>
              )}
              <Bar
                style={{
                  background: color.toCssValue(),
                  height: value ? toPercents(value / maxValue) : "2px",
                }}
              />
              <RelativeWrapper>
                <Label style={{ fontSize: defaultLabelSize }} as="div">
                  {label}
                </Label>
              </RelativeWrapper>
            </Column>
          )
        })}
      </Content>
      <Spacer
        height={`calc(${getCSSUnit(expectedLabelHeight)} + ${barLabelGap})`}
      />
    </VStack>
  )
}

Each item consists of a label, numeric value, color in HSLA format, and an optional function to render value on top of the bar.

To begin, we create a flexbox container with a predetermined height to accommodate the bar chart's elements. Within this container, we include two spacers and the content for the bar chart. The spacers are given fixed heights to prevent any potential overflow issues. This is crucial because we will be positioning the labels and values absolutely in relation to the bars. To make every bar the same width, we will place them inside a CSS grid component. You can learn more about SameWidthChildrenRow here.

Each column represents a flexbox component that contains a RelativeWrapper positioned both below and above the bar. The key aspect is that the RelativeWrapper is a relative element that centrally aligns absolute content within it, without occupying any space within its parent.

Values and labels will have the same font size that should match the expected value or label height, therefore spacers should reserve enough space to prevent labels from overflowing the total height of the component.

The bar itself is a regular div element with a transition property so that when we change items, they would have a nice animation for changing height and color.