Pie, Donut and Polar Area in React

Using ApexCharts with React

This Pie, Donut and Polar Area example wires ApexCharts into React through the react-apexcharts component.

Install it with npm install react-apexcharts apexcharts, then mount the chart with <Chart type="..." options={options} series={series} />.

React charts guide
import React from 'react'
import ReactApexChart from 'react-apexcharts'
import ApexCharts from 'apexcharts'
import './styles.css'

import 'apexcharts/features/highlight-filter'

// Shared by the vanilla-js, React and Vue builds.
//
// Every channel's figure is the whole and one device's is the part. A pick
// is an ordinary update of highlightFilter.data, one value per slice; null
// clears it.
var CHANNELS = ['Organic', 'Paid search', 'Social', 'Email', 'Referral']

// Sums: a channel's sessions and orders are its devices' added up, so a
// device's part always fits inside the channel's slice.
var SESSIONS = [4200, 2600, 1900, 1100, 700]
var SESSIONS_BY_DEVICE = {
  mobile: [2500, 1300, 1500, 400, 300],
  desktop: [1400, 1100, 300, 600, 330],
  tablet: [300, 200, 100, 100, 70],
}
var ORDERS = [300, 240, 160, 180, 120]
var ORDERS_BY_DEVICE = {
  mobile: [140, 100, 100, 60, 45],
  desktop: [130, 120, 45, 100, 60],
  tablet: [30, 20, 15, 20, 15],
}

// Averages: a channel's average order value is its devices' revenue over
// their orders, so a device that spends more per order sits above it.
var ORDER_VALUE = [66, 85, 49, 76, 95]
var ORDER_VALUE_BY_DEVICE = {
  mobile: [52, 70, 41, 60, 76],
  desktop: [81, 98, 66, 84, 109],
  tablet: [70, 85, 55, 80, 95],
}

function pickDevice(device) {
  document.querySelectorAll('[data-device]').forEach(function (b) {
    b.classList.toggle('active', b.getAttribute('data-device') === device)
  })
  // A pick is an ordinary update; null clears it.
  ApexCharts.getChartByID('sessions').updateOptions({
    highlightFilter: { data: device ? SESSIONS_BY_DEVICE[device] : null },
  })
  ApexCharts.getChartByID('orders').updateOptions({
    highlightFilter: { data: device ? ORDERS_BY_DEVICE[device] : null },
  })
  ApexCharts.getChartByID('order-value').updateOptions({
    highlightFilter: { data: device ? ORDER_VALUE_BY_DEVICE[device] : null },
  })
}

function wireDevices() {
  document.querySelectorAll('[data-device]').forEach(function (b) {
    b.addEventListener('click', function () {
      pickDevice(b.getAttribute('data-device'))
    })
  })
}

const ApexChart = () => {
  const [state, setState] = React.useState({
    series: SESSIONS,
    options: {
      chart: {
        id: 'sessions',
        type: 'donut',
        height: 360,
        toolbar: { show: false },
      },
      labels: CHANNELS,
      colors: ['#0284c7', '#0f766e', '#f59e0b', '#e11d48', '#64748b'],
      plotOptions: {
        pie: {
          donut: {
            size: '58%',
            labels: {
              show: true,
              value: {
                formatter: function (v) {
                  return Number(v).toLocaleString()
                },
              },
              total: {
                show: true,
                label: 'Sessions',
                formatter: function (w) {
                  return w.globals.seriesTotals
                    .reduce(function (a, b) {
                      return a + b
                    }, 0)
                    .toLocaleString()
                },
              },
            },
          },
        },
      },
      // No slice borders: the faded tints alone keep the slices apart.
      stroke: { width: 0 },
      dataLabels: { enabled: true },
      legend: { position: 'bottom' },
      title: { text: 'Sessions by channel', style: { fontSize: '13px' } },
      tooltip: {
        y: {
          formatter: function (v) {
            return Number(v).toLocaleString()
          },
        },
      },
      highlightFilter: { data: SESSIONS_BY_DEVICE.mobile },
    },
    series1: ORDERS,
    options1: {
      chart: {
        id: 'orders',
        type: 'polarArea',
        height: 360,
        toolbar: { show: false },
      },
      labels: CHANNELS,
      colors: ['#0284c7', '#0f766e', '#f59e0b', '#e11d48', '#64748b'],
      fill: { opacity: 0.85 },
      stroke: { width: 0 },
      legend: { position: 'bottom' },
      title: { text: 'Orders by channel', style: { fontSize: '13px' } },
      highlightFilter: { data: ORDERS_BY_DEVICE.mobile },
    },
    series2: ORDER_VALUE,
    options2: {
      chart: {
        id: 'order-value',
        type: 'polarArea',
        height: 360,
        toolbar: { show: false },
      },
      labels: CHANNELS,
      colors: ['#0284c7', '#0f766e', '#f59e0b', '#e11d48', '#64748b'],
      fill: { opacity: 0.85 },
      stroke: { width: 0 },
      yaxis: {
        labels: {
          formatter: function (v) {
            return '$' + v
          },
        },
      },
      legend: { position: 'bottom' },
      title: {
        text: 'Average order value by channel',
        style: { fontSize: '13px' },
      },
      tooltip: {
        y: {
          formatter: function (v) {
            return '$' + v
          },
        },
      },
      highlightFilter: { data: ORDER_VALUE_BY_DEVICE.mobile },
    },
  })

  React.useEffect(() => {
    // The react-apexcharts wrappers own the renders, so reach the live
    // instances by their chart.id. (pickDevice lives in the shared head script.)
    const timer = window.setInterval(() => {
      if (
        !ApexCharts.getChartByID('sessions') ||
        !ApexCharts.getChartByID('orders') ||
        !ApexCharts.getChartByID('order-value')
      )
        return
      window.clearInterval(timer)
      wireDevices()
    }, 50)
    return () => window.clearInterval(timer)
  }, [])

  return (
    <div>
      <div className="wrap">
        <h1>Which device brings the traffic, and the bigger orders?</h1>
        <p>
          Pick a device. Each channel stays faded and the device's share is
          drawn solid in front.
        </p>
        <div className="actions">
          <span>Device</span>
          <button data-device="mobile" className="active">
            Mobile
          </button>
          <button data-device="desktop">Desktop</button>
          <button data-device="tablet">Tablet</button>
          <button data-device="">All (clear)</button>
        </div>

        <h2>
          <small>Sums</small>The device is a share of the channel
        </h2>
        <p>
          A channel's sessions and orders are its devices' added up, so the
          solid part always fits inside its slice.
        </p>
        <div className="grid">
          <div className="card">
            <div id="chart">
              <ReactApexChart
                options={state.options}
                series={state.series}
                type="donut"
                height={360}
              />
            </div>
          </div>
          <div className="card">
            <div id="chart2">
              <ReactApexChart
                options={state.options1}
                series={state.series1}
                type="polarArea"
                height={360}
              />
            </div>
          </div>
        </div>

        <h2>
          <small>Averages</small>A device can spend more than its channel
        </h2>
        <p>
          An average is not a sum, so a device can sit above its channel. Pick
          Desktop: each solid slice runs past the channel's value, marked by a
          dashed line.
        </p>
        <div className="grid">
          <div className="card">
            <div id="chart3">
              <ReactApexChart
                options={state.options2}
                series={state.series2}
                type="polarArea"
                height={360}
              />
            </div>
          </div>
        </div>
      </div>
    </div>
  )
}

export default ApexChart