Gauges and Radial Bars in React

Using ApexCharts with React

This Gauges and Radial Bars 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.
//
// The company's figures are the wholes and one region's are the parts. A
// pick is an ordinary update of highlightFilter.data, one value per ring;
// null clears it.
//
// Sums: the company's figure is the regions added up, so every part fits
// inside its whole. Orders shipped this month (against a target of 12,000),
// and each team's progress toward its quarterly goal, split by the region
// that contributed it (the three regions add up to the team's figure).
var TARGET = 12000
var SHIPPED = 9300
var SHIPPED_BY_REGION = { americas: 4100, europe: 3200, apac: 2000 }
var TEAMS = ['Sales', 'Support', 'Product']
var GOALS = [72, 58, 85]
var GOALS_BY_REGION = {
  americas: [32, 20, 38],
  europe: [24, 22, 27],
  apac: [16, 16, 20],
}

// Averages: a score or a rate over every region, which one region can beat.
// Customer satisfaction (split in lanes) and tickets resolved the same day
// (a second needle).
var SATISFACTION = 78
var SATISFACTION_BY_REGION = { americas: 84, europe: 71, apac: 66 }
var SAME_DAY = 64
var SAME_DAY_BY_REGION = { americas: 71, europe: 58, apac: 49 }

function pickRegion(region) {
  document.querySelectorAll('[data-region]').forEach(function (b) {
    b.classList.toggle('active', b.getAttribute('data-region') === region)
  })
  ApexCharts.getChartByID('shipped').updateOptions({
    highlightFilter: { data: region ? [SHIPPED_BY_REGION[region]] : null },
  })
  ApexCharts.getChartByID('goals').updateOptions({
    highlightFilter: { data: region ? GOALS_BY_REGION[region] : null },
  })
  ApexCharts.getChartByID('satisfaction').updateOptions({
    highlightFilter: { data: region ? [SATISFACTION_BY_REGION[region]] : null },
  })
  ApexCharts.getChartByID('same-day').updateOptions({
    highlightFilter: { data: region ? [SAME_DAY_BY_REGION[region]] : null },
  })
}

function wireRegions() {
  document.querySelectorAll('[data-region]').forEach(function (b) {
    b.addEventListener('click', function () {
      pickRegion(b.getAttribute('data-region'))
    })
  })
}

const ApexChart = () => {
  const [state, setState] = React.useState({
    series: [SHIPPED],
    options: {
      chart: {
        id: 'shipped',
        type: 'gauge',
        height: 300,
        toolbar: { show: false },
      },
      labels: ['Orders shipped'],
      colors: ['#0284c7'],
      plotOptions: {
        radialBar: {
          // A count on its own domain: 0 to the month's target.
          min: 0,
          max: TARGET,
          dataLabels: {
            value: {
              fontSize: '28px',
              formatter: function (v) {
                return Number(v).toLocaleString()
              },
            },
          },
        },
      },
      tooltip: {
        y: {
          formatter: function (v) {
            return Number(v).toLocaleString()
          },
        },
      },
      title: {
        text: 'Orders shipped this month, target 12,000',
        style: { fontSize: '13px' },
      },
      highlightFilter: { data: [SHIPPED_BY_REGION.americas] },
    },
    series1: GOALS,
    options1: {
      chart: {
        id: 'goals',
        type: 'radialBar',
        height: 340,
        toolbar: { show: false },
      },
      labels: TEAMS,
      colors: ['#0284c7', '#f59e0b', '#e11d48'],
      plotOptions: {
        radialBar: {
          hollow: { size: '38%' },
          track: { margin: 4 },
          dataLabels: {
            name: { fontSize: '13px' },
            value: { fontSize: '20px' },
            total: {
              show: true,
              label: 'Average',
              // While a region is picked the totals read its parts (the default
              // highlightFilter.dataLabels.total: 'part'), so this averages the
              // region's contributions; cleared, the teams' progress.
              formatter: function (w) {
                var t = w.globals.seriesTotals
                return (
                  Math.round(
                    t.reduce(function (a, b) {
                      return a + b
                    }, 0) / t.length,
                  ) + '%'
                )
              },
            },
          },
        },
      },
      legend: { show: true, position: 'bottom' },
      title: {
        text: 'Quarterly goal reached, by team',
        style: { fontSize: '13px' },
      },
      highlightFilter: { data: GOALS_BY_REGION.americas },
    },
    series2: [SATISFACTION],
    options2: {
      chart: {
        id: 'satisfaction',
        type: 'gauge',
        height: 300,
        toolbar: { show: false },
      },
      labels: ['Satisfaction'],
      colors: ['#0f766e'],
      plotOptions: {
        radialBar: {
          dataLabels: {
            value: {
              fontSize: '28px',
              // The centre reads the region; while one is picked, a second line
              // under it reads the whole (opts.lane is 'whole' for that line).
              formatter: function (v, w, opts) {
                return (
                  (opts && opts.lane === 'whole' ? 'company ' : '') + v + '%'
                )
              },
            },
          },
        },
      },
      title: { text: 'Customer satisfaction', style: { fontSize: '13px' } },
      highlightFilter: {
        data: [SATISFACTION_BY_REGION.americas],
        // Made for a part that can pass its whole: two lanes, never overlapping.
        radialBar: { indicator: 'lanes' },
      },
    },
    series3: [SAME_DAY],
    options3: {
      chart: {
        id: 'same-day',
        type: 'gauge',
        height: 300,
        toolbar: { show: false },
      },
      labels: ['Same-day resolution'],
      colors: ['#ea580c'],
      plotOptions: {
        radialBar: {
          shape: 'needle',
          needle: { length: '78%' },
          ticks: {
            show: true,
            major: { count: 6, length: 6 },
            minor: { count: 1, length: 3 },
            labels: { show: true, fontSize: '10px' },
          },
          dataLabels: { value: { fontSize: '22px' } },
        },
      },
      title: {
        text: 'Tickets resolved the same day',
        style: { fontSize: '13px' },
      },
      highlightFilter: { data: [SAME_DAY_BY_REGION.americas] },
    },
  })

  React.useEffect(() => {
    // The react-apexcharts wrappers own the renders, so reach the live
    // instances by their chart.id. (pickRegion lives in the shared head script.)
    const timer = window.setInterval(() => {
      if (
        !ApexCharts.getChartByID('shipped') ||
        !ApexCharts.getChartByID('goals') ||
        !ApexCharts.getChartByID('satisfaction') ||
        !ApexCharts.getChartByID('same-day')
      )
        return
      window.clearInterval(timer)
      wireRegions()
    }, 50)
    return () => window.clearInterval(timer)
  }, [])

  return (
    <div>
      <div className="wrap">
        <h1>How does one region compare with the whole company?</h1>
        <p>
          Pick a region. The company's figure stays faded and the region's is
          drawn solid in front. Hover any arc or needle for both values.
        </p>
        <div className="actions">
          <span>Region</span>
          <button data-region="americas" className="active">
            Americas
          </button>
          <button data-region="europe">Europe</button>
          <button data-region="apac">Asia Pacific</button>
          <button data-region="">All (clear)</button>
        </div>

        <h2>
          <small>Sums</small>The region is a share of the company
        </h2>
        <p>
          The company's figure is the three regions added up, so a region's arc
          always ends inside it. On the rings, the solid arc is the region's
          contribution to each team's goal.
        </p>
        <div className="grid">
          <div className="card">
            <div id="chart">
              <ReactApexChart
                options={state.options}
                series={state.series}
                type="gauge"
                height={300}
              />
            </div>
          </div>
          <div className="card">
            <div id="chart2">
              <ReactApexChart
                options={state.options1}
                series={state.series1}
                type="radialBar"
                height={340}
              />
            </div>
          </div>
        </div>

        <h2>
          <small>Averages</small>A region can beat the company
        </h2>
        <p>
          A score or a rate is an average, so a region can sit above the
          company. The satisfaction gauge uses <code>indicator: 'lanes'</code>{' '}
          to draw the two side by side; the needle gauge fades the company's
          needle.
        </p>
        <div className="grid">
          <div className="card">
            <div id="chart3">
              <ReactApexChart
                options={state.options2}
                series={state.series2}
                type="gauge"
                height={300}
              />
            </div>
          </div>
          <div className="card">
            <div id="chart4">
              <ReactApexChart
                options={state.options3}
                series={state.series3}
                type="gauge"
                height={300}
              />
            </div>
          </div>
        </div>
      </div>
    </div>
  )
}

export default ApexChart