Through a Forecast in Vue

Using ApexCharts with Vue

This Through a Forecast 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>Highlight a part through a forecast</h1>
      <p>
        Pick a region. Each plan's subscriptions stay on the charts at their
        full value, faded in the stacked columns and dashed on the step line,
        and the region's share is drawn solid. The last three months are a
        forecast: the part keeps the forecast's dashes and lighter fill there
        too.
      </p>
      <div class="actions">
        <span>Region</span>
        <button data-region="europe" class="active">Europe</button>
        <button data-region="americas">Americas</button>
        <button data-region="">All (clear)</button>
      </div>
      <div class="chart-wrap">
        <div id="chart">
          <apexchart
            type="bar"
            height="320"
            :options="chartOptions"
            :series="series"
          ></apexchart>
        </div>
      </div>
      <div class="chart-wrap">
        <div id="chart2">
          <apexchart
            type="line"
            height="320"
            :options="chartOptions1"
            :series="series1"
          ></apexchart>
        </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.
//
// New subscriptions per month by plan (the wholes), and the ones sold in one
// region (the parts). The last three months are a forecast. Picking a region
// keeps every plan on both charts: the whole is dashed or faded, the region's
// share is solid, and past the forecast cut both keep the forecast look.
var MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep']
var PLANS = ['Team', 'Personal']
var SUBSCRIPTIONS = [
  [120, 132, 128, 145, 150, 162, 170, 178, 186],
  [80, 84, 90, 88, 96, 101, 104, 110, 114]
]
var REGIONS = {
  europe: [
    [52, 58, 55, 63, 66, 71, 74, 78, 81],
    [30, 33, 35, 34, 38, 40, 41, 43, 45]
  ],
  americas: [
    [48, 51, 50, 57, 58, 63, 66, 69, 72],
    [36, 37, 40, 39, 42, 44, 46, 49, 50]
  ]
}

function seriesFor(region) {
  return PLANS.map(function (name, i) {
    var s = { name: name, data: SUBSCRIPTIONS[i] }
    if (region) s.highlightData = REGIONS[region][i]
    return s
  })
}

// One call per chart and pick: the parts go in, each chart animates from what
// is on screen. Clearing is the same call without them.
function pickRegion(region) {
  document.querySelectorAll('[data-region]').forEach(function (b) {
    b.classList.toggle('active', b.getAttribute('data-region') === region)
  })
  return Promise.all(
    ['plans-stacked', 'plans-line'].map(function (id) {
      return ApexCharts.getChartByID(id).updateSeries(seriesFor(region))
    })
  )
}

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: seriesFor('europe'),
chartOptions: {
chart: {
  id: 'plans-stacked',
  type: 'bar',
  height: 320,
  stacked: true,
  toolbar: { show: false },
},
colors: ['#0f766e', '#d97706'],
plotOptions: {
  bar: { columnWidth: '55%', borderRadius: 4, borderRadiusApplication: 'end' },
},
forecastDataPoints: { count: 3, fillOpacity: 0.5, dashArray: 4, strokeWidth: 1 },
dataLabels: { enabled: false },
xaxis: { categories: MONTHS },
yaxis: { title: { text: 'New subscriptions' } },
legend: { position: 'bottom' },
tooltip: { shared: true, intersect: false },
},
series1: seriesFor('europe'),
chartOptions1: {
chart: {
  id: 'plans-line',
  type: 'line',
  height: 320,
  toolbar: { show: false },
},
colors: ['#0f766e', '#d97706'],
stroke: { curve: 'stepline', width: 3 },
forecastDataPoints: { count: 3, dashArray: 6 },
dataLabels: { enabled: false },
xaxis: { categories: MONTHS },
yaxis: { min: 0, title: { text: 'New subscriptions' } },
legend: { position: 'bottom' },
tooltip: { shared: true, intersect: false },
},
regionTimer: null,
}
},
mounted: function () {
  // The vue-apexcharts wrapper owns the render, so reach the live instances by
  // their chart.id. (seriesFor / pickRegion live in the shared head script.)
  var me = this
  me.regionTimer = window.setInterval(function () {
    if (!ApexCharts.getChartByID('plans-line') || !ApexCharts.getChartByID('plans-stacked')) 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: 720px;
  margin: 0 auto;
}
h1 {
  font-size: 22px;
  margin: 0 0 8px;
}
p {
  color: #5b6b78;
  line-height: 1.5;
  margin: 0 0 20px;
}
.chart-wrap {
  background: #fff;
  border-radius: 8px;
  padding: 16px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.04);
  margin: 0 0 16px;
}
.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: #0f766e;
  border-color: #0f766e;
}
</style>