Filter by a Product in Vue

Using ApexCharts with Vue

This Filter by a Product 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>Filter by a product, keep the whole picture</h1>
      <p>
        Click a product on the left. The quarters on the right keep every
        region's full revenue, faded, with that product's share solid in front,
        so you see both how much it sold and how much of the business it is.
      </p>
      <div class="readout" id="readout">
        Click a product to filter the quarters by it.
      </div>
      <div class="grid">
        <div class="card">
          <div id="chart">
            <apexchart
              type="bar"
              height="320"
              :options="chartOptions"
              :series="series"
            ></apexchart>
          </div>
        </div>
        <div class="card">
          <div id="chart2">
            <apexchart
              type="bar"
              height="320"
              :options="chartOptions1"
              :series="series1"
            ></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.
//
// Revenue ($k) per product, region and quarter. The left chart sums it per
// product; the right one per region and quarter. Clicking a product filters
// the right chart BY it without dropping anything: every column keeps its
// full revenue, faded, and the product's share is drawn solid in front.
var PRODUCTS = ['Laptops', 'Phones', 'Tablets', 'Audio']
var REGIONS = ['North', 'South']
var QUARTERS = ['Q1', 'Q2', 'Q3', 'Q4']
var REVENUE = {
  Laptops: { North: [42, 38, 45, 61], South: [30, 28, 33, 44] },
  Phones: { North: [55, 49, 52, 70], South: [48, 51, 47, 66] },
  Tablets: { North: [18, 15, 17, 26], South: [22, 19, 21, 25] },
  Audio: { North: [12, 14, 13, 21], South: [9, 11, 10, 16] },
}

function sum(a) {
  return a.reduce(function (s, v) {
    return s + v
  }, 0)
}
function productTotal(p) {
  return sum(
    REGIONS.map(function (r) {
      return sum(REVENUE[p][r])
    }),
  )
}
function regionQuarters(r, onlyProduct) {
  return QUARTERS.map(function (q, k) {
    return sum(
      PRODUCTS.filter(function (p) {
        return !onlyProduct || p === onlyProduct
      }).map(function (p) {
        return REVENUE[p][r][k]
      }),
    )
  })
}

// The picked product, or null.
var picked = null

function productSeries() {
  var totals = PRODUCTS.map(productTotal)
  var s = { name: 'Revenue', data: totals }
  // On the source chart the pick is the whole of its own column, so it stays
  // solid while the others fade.
  if (picked) {
    s.highlightData = PRODUCTS.map(function (p, j) {
      return p === picked ? totals[j] : null
    })
  }
  return [s]
}

function quarterSeries() {
  return REGIONS.map(function (r) {
    var s = { name: r, data: regionQuarters(r) }
    if (picked) s.highlightData = regionQuarters(r, picked)
    return s
  })
}

function pickProduct(index) {
  var product = PRODUCTS[index]
  picked = picked === product ? null : product
  var readout = document.getElementById('readout')
  if (readout) {
    readout.innerHTML = picked
      ? 'Filtered by <b>' + picked + '</b>. Click it again to clear.'
      : 'Click a product to filter the quarters by it.'
  }
  // Let the click handler unwind before both charts redraw.
  Promise.resolve().then(function () {
    ApexCharts.getChartByID('products').updateSeries(productSeries())
    ApexCharts.getChartByID('quarters').updateSeries(quarterSeries())
  })
}

export default {
  components: {
    apexchart: VueApexCharts,
  },
  data: function () {
    return {
      series: productSeries(),
      chartOptions: {
        chart: {
          id: 'products',
          type: 'bar',
          height: 320,
          toolbar: { show: false },
          events: {
            dataPointSelection: function (e, ctx, opts) {
              pickProduct(opts.dataPointIndex)
            },
          },
        },
        colors: ['#0f766e'],
        plotOptions: {
          bar: { horizontal: true, barHeight: '64%', borderRadius: 3 },
        },
        states: { active: { filter: { type: 'none' } } },
        dataLabels: { enabled: false },
        xaxis: {
          categories: PRODUCTS,
          tickAmount: 3,
          labels: {
            formatter: function (v) {
              return '$' + v + 'k'
            },
          },
        },
        // On the source chart the pick is its own column, so a tooltip row for the
        // part would only repeat the total.
        highlightFilter: {
          tooltip: { show: false },
          dataLabels: { value: 'whole' },
        },
        legend: { show: false },
        title: { text: 'Revenue by product', style: { fontSize: '13px' } },
        tooltip: {
          y: {
            formatter: function (v) {
              return '$' + v + 'k'
            },
          },
        },
      },
      series1: quarterSeries(),
      chartOptions1: {
        chart: {
          id: 'quarters',
          type: 'bar',
          height: 320,
          toolbar: { show: false },
        },
        colors: ['#2563eb', '#f59e0b'],
        plotOptions: {
          bar: { columnWidth: '70%', borderRadius: 3 },
        },
        dataLabels: { enabled: false },
        stroke: { show: true, width: 1, colors: ['transparent'] },
        xaxis: { categories: QUARTERS },
        yaxis: {
          labels: {
            formatter: function (v) {
              return '$' + v + 'k'
            },
          },
        },
        legend: { position: 'bottom' },
        title: { text: 'Revenue by quarter', style: { fontSize: '13px' } },
        tooltip: {
          shared: true,
          intersect: false,
          y: {
            formatter: function (v) {
              return '$' + v + 'k'
            },
          },
        },
      },
    }
  },
}
</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 16px;
}
.grid {
  display: grid;
  grid-template-columns: 300px 1fr;
  gap: 16px;
}
.card {
  background: #fff;
  border-radius: 8px;
  padding: 12px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.04);
  min-width: 0;
}
.readout {
  font-size: 13px;
  color: #5b6b78;
  margin: 0 0 12px;
  min-height: 18px;
}
.readout b {
  color: #2c3e50;
}
@media (max-width: 720px) {
  .grid {
    grid-template-columns: 1fr;
  }
}
</style>