Gauges and Radial Bars in Vue

Using ApexCharts with Vue

This Gauges and Radial Bars 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>