Eric
Howey.

There is a hidden page you can visit at /you-are-awesome. Didn't want you to miss out on the fun!

Using Next.js and Tremor for charts, graphs, and data visualization

Charts, graphs, and data visualizations are an essential tool when presenting data or explaining complex topics more clearly. They are widely used in websites, apps, and other digital media. But creating these charts is not always easy with lots of potential gotcha moments.

I have tried a lot of charting and data visualization libraries and found that Tremor strikes the right balance between configurability, good design, and happy defaults.

Tremor makes it easy to land in the pit of success.

That isn’t to say it is perfect for all use cases. By being opinionated Tremor makes the trade off of being less configurable than some other lower level data visualization libraries. Tremor is also tightly integrated with Tailwind for styling which can be a dealbreaker for some and a bonus for others. Under the hood Tremor relies on a library called Recharts for creating the actual charts.

Getting setup

If you don’t already have a Next.js project with Tailwind installed and configured then you can follow their getting started guide, which will preconfigure Next.js and Tailwind for you.

If you have a greenfield project you can use npx @tremor/cli@latest init to quickly install Tremor. If you have an existing project you should follow their installation instructions so you don’t accidentally overwrite your existing tailwind.config.ts file.

Our first chart

My son is in grade 3 and recently had to make a bar chart of his classmates’ favourite kinds of weather. So let’s start there!

Data for Tremor charts has index values which are the values used as labels on the axis. It also has what are called categories which are the values actually plotted on the graph. The data format can vary slightly based on what chart you are using so be sure to check their docs.

In this example our index values will be the different types of weather and our single category will be the number of classmates.

BarChart.tsx
import { BarChart, Card } from '@tremor/react'

const data = [
  {
    Weather: 'Sunny',
    Classmates: 10,
  },
  {
    Weather: 'Snowy',
    Classmates: 5,
  },
  {
    Weather: 'Cloudy',
    Classmates: 4,
  },
  {
    Weather: 'Rainy',
    Classmates: 2,
  },
  {
    Weather: 'Foggy',
    Classmates: 1,
  },
]

export default function BarChartDemo() {
  return (
    <div className="my-10">
      <Card>
        <BarChart data={data} index="Weather" categories={['Classmates']} />
      </Card>
    </div>
  )
}

And here is our basic bar chart.

Using chart variations

One thing to know about Tremor is that it has chart variations, so there is no “pie chart” but rather it is a variation of the DonutChart component. Another common chart is a stream chart, this is a variation on the AreaChart component.

Here is our same data, from above, but this time rendered as a pie chart. Notice that instead of categories it only accepts a single category prop because the donut chart does not support multiple categories like some other charts do. And we have specified variant="pie" to render it as a pie chart.

DonutChart.tsx
import { DonutChart, Card } from '@tremor/react'

const data = [
  {
    Weather: 'Sunny',
    Classmates: 10,
  },
  /* Same data as above... */
]

export default function DonutChartDemo() {
  return (
    <div className="my-10">
      <Card>
        <DonutChart
          data={data}
          index="Weather"
          category="Classmates"
          variant="pie"
        />
      </Card>
    </div>
  )
}

And our delicious pie chart!

Using the valueFormatter prop

Our pie chart is looking pretty tasty but it would be a lot better if we could show percentages instead of just the raw number of classmates. Most Tremor charts accept a valueFormatter prop to customize how the values are displayed. This can let you do things like round numbers, add currency symbols, or as we are going to do convert the numbers to percentages.

DonutChart.tsx
import { DonutChart, Card } from '@tremor/react'

const data = [
  /* Same data as above... */
]

const valueFormatter = (number: number) => {
  // Use reduce method to calculate total number of classmates
  const totalClassmates = data.reduce(
    (accumulator, currentValue) => accumulator + currentValue.Classmates,
    0,
  )
  // Calculate the percentage and round it to a whole number
  const percentage = Math.round((number / totalClassmates) * 100)
  // Return a string
  return `${percentage}%`
}

export default function DonutChartDemo() {
  return (
    <div className="my-10">
      <Card>
        <DonutChart
          data={data}
          valueFormatter={valueFormatter}
          index="Weather"
          category="Classmates"
          variant="pie"
        />
      </Card>
    </div>
  )
}

And here is our chart with easier to understand percentage values.

Adding a custom tooltip

Tremor has recently added the ability to customize the tooltip that appears on hover. This takes things a step beyond the value formatter prop allowing you to tweak not just the appearance of the numbers displayed but the appearance of the entire tooltip itself.

The default tooltip

Tremor includes a great default tooltip and odds are you will want to simply extend the default tooltip so that your styles remain consistent across your charts.

The default tooltips differ a bit from chart to chart so your mileage may vary. This is the default from an area chart.

const defaultTooltip = ({ payload, active, label }) => {
  if (!active || !payload) return null
  return (
    <div className="rounded-tremor-default text-tremor-default bg-tremor-background shadow-tremor-dropdown border-tremor-border dark:bg-dark-tremor-background dark:shadow-dark-tremor-dropdown dark:border-dark-tremor-border border">
      <div className="border-tremor-border dark:border-dark-tremor-border border-b px-4 py-2">
        <p className="text-tremor-content-emphasis dark:text-dark-tremor-content-emphasis font-medium">
          {label}
        </p>
      </div>
      <div className="space-y-1 px-4 py-2">
        {payload.map((category, idx) => (
          <div key={idx} class="flex items-center justify-between space-x-8">
            <div className="flex items-center space-x-2">
              <span
                className={`rounded-tremor-full border-tremor-background shadow-tremor-card dark:border-dark-tremor-background dark:shadow-dark-tremor-card h-3 w-3 shrink-0 border-2 bg-${category.color}-500`}
              ></span>
              <p className="text-tremor-content dark:text-dark-tremor-content whitespace-nowrap text-right">
                {category.dataKey}
              </p>
            </div>
            <p className="text-tremor-content-emphasis dark:text-dark-tremor-content-emphasis whitespace-nowrap text-right font-medium tabular-nums">
              {category.value}
            </p>
          </div>
        ))}
      </div>
    </div>
  )
}

Being able to customize the tooltip means you can do all sorts of nifty things like add icons, calculate composite scores, add explainers, and more.

Let’s add some simple weather icons to our pie chart and show scores as a fraction this time!

Making a custom tooltip

The two props to pay attention to here are the label and the payload. The label is linked with the index prop from Tremor. The payload is linked to the category prop from Tremor. It can be worth quickly console logging these values because the payload will vary slightly between chart types depending on how many categories are assigned. Note that this example includes the types for the custom tooltip as well.

DonutChart.tsx
import { DonutChart, Card } from '@tremor/react'
import { type TooltipProps } from 'recharts'
import type {
  NameType,
  ValueType,
} from 'recharts/types/component/DefaultTooltipContent'
import { CloudIcon, FogIcon, RainIcon, SnowIcon, SunIcon } from './logos'

const data = [
  {
    Weather: 'Sunny',
    Classmates: 10,
    icon: SunIcon,
  },
  /* ...rest of data */
]

const customTooltip = ({
  payload,
  active,
  label,
}: TooltipProps<ValueType, NameType>) => {
  if (!active || !payload) return null
  const Icon = payload[0].payload.icon
  const totalClassmates = data.reduce(
    (accumulator, currentValue) => accumulator + currentValue.Classmates,
    0,
  )
  return (
    <div className="rounded-tremor-default text-tremor-default bg-tremor-background shadow-tremor-dropdown border-tremor-border dark:bg-dark-tremor-background dark:shadow-dark-tremor-dropdown dark:border-dark-tremor-border border">
      <div className="border-tremor-border dark:border-dark-tremor-border inline-flex items-center border-b px-4 py-2">
        <Icon className="mr-2 h-5 w-5" />
        <p className="text-tremor-content-emphasis dark:text-dark-tremor-content-emphasis font-medium">
          {label}
        </p>
      </div>
      <div className="space-y-1 px-4 py-2">
        {payload.map((category, idx) => (
          <div
            className="flex items-center justify-between space-x-8"
            key={idx}
          >
            <div className="flex items-center space-x-2">
              <span
                className={`rounded-tremor-full border-tremor-background shadow-tremor-card dark:border-dark-tremor-background dark:shadow-dark-tremor-card h-3 w-3 shrink-0 border-2 bg-${category.color}-500`}
              ></span>
              <p className="text-tremor-content dark:text-dark-tremor-content whitespace-nowrap text-right">
                {category.dataKey}
              </p>
            </div>
            <p className="text-tremor-content-emphasis dark:text-dark-tremor-content-emphasis whitespace-nowrap text-right font-medium tabular-nums">
              {category.value} / {totalClassmates}
            </p>
          </div>
        ))}
      </div>
    </div>
  )
}

export default function DonutChartDemo() {
  return (
    <div className="not-prose my-10">
      <Card>
        <DonutChart
          data={data}
          index="Weather"
          category="Classmates"
          variant="pie"
          customTooltip={customTooltip}
        />
      </Card>
    </div>
  )
}

And voila, our chart with an improved tooltip!

Interactive charts with click events

Tremor makes it very easy to add basic click events to your chart and exposes this data for further customization and interactivity.

All you need to do is add a setState call to the onValueChange prop and your chart becomes interactive and that value is exposed to you as a developer. You can console.log(value) to see the data that is available.

This is a recent addition to Tremor and there is some magic happening here behind the scenes I’m not fully comfortable with. I was expecting to have to manually configure the CSS classes of the chart lines based on the event callback but Tremor does all of this for you. This is a great example of the trade off in Tremor between ease-of-use and configurability.

Here is an example of click events using an area chart for the weather.

AreaChart.tsx
import { AreaChart, Card, type EventProps } from '@tremor/react'
import { useState } from 'react'

const data = [
  {
    month: 'January',
    rain: 0.2,
    snow: 3.7,
  },
  //...more data
  {
    month: 'December',
    rain: 0.3,
    snow: 26.1,
  },
]

export default function AreaChartDemo() {
  const [value, setValue] = useState<null | EventProps>(null)
  return (
    <div className="not-prose my-10">
      <Card>
        <AreaChart
          data={data}
          index="month"
          categories={['rain', 'snow']}
          onValueChange={(v: EventProps) => setValue(v)}
        />
      </Card>
    </div>
  )
}

And here is the chart we get with click events on both the lines and the legend.

Using Next.js dynamic imports for better performance

Charts can be resource intensive to load on the client and using Next.js dynamic imports is a way to decrease the time-to-interactive score and improve overall performance. Depending on how you are using charts this may not be necessary, but it is an option to consider. Behind the scenes Next.js uses React.lazy() and the Suspense API to show an initial loading state and then display the chart after it has finished loading.

const ChartLoader = () => (
  <div
    role="status"
    className="animate-pulse rounded border border-gray-200 p-4"
  >
    <div className="grid h-80 place-items-center">
      <span className="text-2xl font-semibold">Loading...</span>
    </div>
  </div>
)

const DonutChart = dynamic(
  () =>
    import('~/components/charts/DonutChart')
  {
    loading: ChartLoader,
    ssr: false,
  },
)

Usage with @tailwindcss/typography

This one is a simple, but important tip. If your charts will be contained within a block of prose content from Tailwind you will want to use the not-prose class on a wrapper around your chart. This prevents the Tailwind prose classes from affecting the visual appearance of your chart.

<div className="not-prose">
  <MyChart />
</div>

All the other bits

Tremor is slowly expanding its library of components with the goal of being the React library to build dashboards fast. My personal experience has been that its utility components like <Title> and <Subtitle> make prettying up your charts fast and painless. Tremor also includes a bunch of nice progress trackers, inputs, and helper components that are useful when creating dashboards and presenting data.

My experience has been that Tremor hits a sweet spot of developer experience, great visual design, and happy defaults.

I hope this has helped your data visualization journey. Happy coding and happy charting!