Pie, Donut and Polar Area in Vue

Using ApexCharts with Vue

This Pie, Donut and Polar Area 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>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 class="actions">
        <span>Device</span>
        <button data-device="mobile" class="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 class="grid">
        <div class="card">
          <div id="chart">
            <apexchart
              type="donut"
              height="360"
              :options="chartOptions"
              :series="series"
            ></apexchart>
          </div>
        </div>
        <div class="card">
          <div id="chart2">
            <apexchart
              type="polarArea"
              height="360"
              :options="chartOptions1"
              :series="series1"
            ></apexchart>
          </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 class="grid">
        <div class="card">
          <div id="chart3">
            <apexchart
              type="polarArea"
              height="360"
              :options="chartOptions2"
              :series="series2"
            ></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.
//
// 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'))
    })
  })
}

export default {
components: {
apexchart: VueApexCharts,
},
data: function () {
return {
series: SESSIONS,
chartOptions: {
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,
chartOptions1: {
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,
chartOptions2: {
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 },
},
deviceTimer: null,
}
},
mounted: function () {
  // The vue-apexcharts wrappers own the renders, so reach the live instances
  // by their chart.id. (pickDevice lives in the shared head script.)
  var me = this
  me.deviceTimer = window.setInterval(function () {
    if (
      !ApexCharts.getChartByID('sessions') ||
      !ApexCharts.getChartByID('orders') ||
      !ApexCharts.getChartByID('order-value')
    ) return
    window.clearInterval(me.deviceTimer)
    wireDevices()
  }, 50)
},
beforeDestroy: function () {
  window.clearInterval(this.deviceTimer)
},,
}
</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: 760px;
  margin: 0 auto;
}
h1 {
  font-size: 22px;
  margin: 0 0 8px;
}
p {
  color: #5b6b78;
  line-height: 1.5;
  margin: 0 0 20px;
}
.actions {
  margin: 0 0 14px;
  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;
}
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;
}
.grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 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;
}
/* Each chart fills its card up to 340px, centred, and shrinks with it on a
   phone. */
.card > div {
  max-width: 340px;
  margin: 0 auto;
}
@media (max-width: 720px) {
  .grid {
    grid-template-columns: minmax(0, 1fr);
  }
}
</style>