Gauges and Radial Bars with One Region Highlighted in React

Using ApexCharts with React

This Gauges and Radial Bars with One Region Highlighted 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