Gauges and Radial Bars with One Region Highlighted in Vue

Using ApexCharts with Vue

This Gauges and Radial Bars with One Region Highlighted example wires ApexCharts into Vue through the vue3-apexcharts component.

Install it with npm install vue3-apexcharts apexcharts, then mount the chart with <apexchart type="..." :options="options" :series="series" />.

Vue charts guide
<template>
  <div>
    <div class="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 class="actions">
        <span>Region</span>
        <button data-region="americas" class="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 class="grid">
        <div class="card">
          <div id="chart">
            <apexchart
              type="gauge"
              height="300"
              :options="chartOptions"
              :series="series"
            ></apexchart>
          </div>
        </div>
        <div class="card">
          <div id="chart2">
            <apexchart
              type="radialBar"
              height="340"
              :options="chartOptions1"
              :series="series1"
            ></apexchart>
          </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 class="grid">
        <div class="card">
          <div id="chart3">
            <apexchart
              type="gauge"
              height="300"
              :options="chartOptions2"
              :series="series2"
            ></apexchart>
          </div>
        </div>
        <div class="card">
          <div id="chart4">
            <apexchart
              type="gauge"
              height="300"
              :options="chartOptions3"
              :series="series3"
            ></apexchart>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import VueApexCharts from 'vue-apexcharts'
import ApexCharts from 'apexcharts'

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'))
    })
  })
}

export default {
  components: {
    apexchart: VueApexCharts,
  },
  data: function () {
    return {
      series: [SHIPPED],
      chartOptions: {
        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,
      chartOptions1: {
        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],
      chartOptions2: {
        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],
      chartOptions3: {
        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] },
      },
      regionTimer: null,
    }
  },
  mounted: function () {
    // The vue-apexcharts wrappers own the renders, so reach the live instances
    // by their chart.id. (pickRegion lives in the shared head script.)
    var me = this
    me.regionTimer = window.setInterval(function () {
      if (
        !ApexCharts.getChartByID('shipped') ||
        !ApexCharts.getChartByID('goals') ||
        !ApexCharts.getChartByID('satisfaction') ||
        !ApexCharts.getChartByID('same-day')
      )
        return
      window.clearInterval(me.regionTimer)
      wireRegions()
    }, 50)
  },
  beforeDestroy: function () {
    window.clearInterval(this.regionTimer)
  },
}
</script>

<style>
body {
  font-family:
    -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: #fafbfc;
  color: #2c3e50;
  margin: 0;
  padding: 32px 16px;
}
.wrap {
  max-width: 960px;
  margin: 0 auto;
}
h1 {
  font-size: 22px;
  margin: 0 0 8px;
}
h2 {
  font-size: 16px;
  margin: 28px 0 6px;
}
h2 small {
  display: block;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: #0284c7;
  margin-bottom: 2px;
}
p {
  color: #5b6b78;
  line-height: 1.5;
  margin: 0 0 12px;
  max-width: 760px;
}
code {
  font-size: 13px;
  background: #eef2f6;
  border-radius: 4px;
  padding: 1px 4px;
}
.actions {
  margin: 8px 0 4px;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
}
.actions span {
  font-size: 13px;
  color: #5b6b78;
  margin-right: 4px;
}
.actions button {
  color: #2c3e50;
  background: #fff;
  border: 1px solid #cfd8e3;
  padding: 6px 14px;
  font-weight: 600;
  font-size: 13px;
  border-radius: 6px;
  cursor: pointer;
}
.actions button.active {
  color: #fff;
  background: #0284c7;
  border-color: #0284c7;
}
.grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 16px;
  align-items: start;
}
.card {
  background: #fff;
  border-radius: 8px;
  padding: 12px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.04);
  min-width: 0;
}
@media (max-width: 680px) {
  .grid {
    grid-template-columns: minmax(0, 1fr);
  }
}
</style>