Population Explorer in Vue

Using ApexCharts with Vue

This Population Explorer 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>Population explorer</h1>
      <p>
        One dot per person, coloured by gender, the minority packed into the
        centre.
      </p>

      <div class="controls">
        Minimum age: <span class="readout" id="readout">45+</span>
        <input type="range" id="age" min="45" max="90" step="1" value="45" />
      </div>

      <div class="chart-wrap">
        <div id="chart">
          <apexchart
            type="unit"
            height="460"
            :options="chartOptions"
            :series="series"
          ></apexchart>
        </div>
      </div>
    </div>
  </div>
</template>

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

import 'apexcharts/unit'

// Shared by the vanilla-js, React and Vue builds. A synthetic population model:
// as the minimum age rises, the crowd shrinks and the female share dips. Each
// dot is one person; colour is gender; the female minority nests in the centre
// of the packed blob. Dragging the slider re-buckets the data and the dots
// redistribute in place, they do not rebuild from scratch.
function countsForAge(minAge) {
  var frac = (90 - minAge) / 45 // 1 at age 45, 0 at age 90
  var total = Math.round(180 + 760 * frac)
  var femaleShare = 0.2 + 0.16 * frac
  var female = Math.round(total * femaleShare)
  return { female: female, male: total - female, total: total }
}

function readoutText(minAge) {
  var c = countsForAge(minAge)
  var pct = ((c.female / c.total) * 100).toFixed(1)
  return (
    c.total.toLocaleString() +
    ' people aged ' +
    minAge +
    '+, ' +
    c.female.toLocaleString() +
    ' female (' +
    pct +
    '%)'
  )
}

export default {
  components: {
    apexchart: VueApexCharts,
  },
  data: function () {
    return {
      series: [338, 602],
      chartOptions: {
        chart: {
          id: 'popChart',
          type: 'unit',
          height: 460,
          animations: {
            enabled: true,
            speed: 700,
          },
        },
        labels: ['Female', 'Male'],
        colors: ['#EE5A8F', '#1AA5A0'],
        legend: {
          position: 'bottom',
        },
        plotOptions: {
          unit: {
            layout: 'packed',
            sortByGroup: true,
            spacing: 1.1,
          },
        },
      },
      popTimer: null,
    }
  },
  mounted: function () {
    // Reach the live instance by its chart.id, then wire the slider.
    // (countsForAge and readoutText live in the shared head script.)
    var me = this
    me.popTimer = window.setInterval(function () {
      var chart = ApexCharts.getChartByID('popChart')
      if (!chart) return
      window.clearInterval(me.popTimer)

      var ageInput = document.querySelector('#age')
      var readout = document.querySelector('#readout')
      ageInput.addEventListener('input', function () {
        var minAge = parseInt(ageInput.value, 10)
        readout.textContent = readoutText(minAge)
        var c = countsForAge(minAge)
        chart.updateSeries([c.female, c.male])
      })
    }, 50)
  },
  beforeDestroy: function () {
    window.clearInterval(this.popTimer)
  },
}
</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);
}
.controls {
  margin: 8px auto 20px;
  max-width: 460px;
  text-align: center;
  color: #5b6b78;
  font-size: 14px;
}
.controls input[type='range'] {
  width: 100%;
  margin-top: 8px;
}
.readout {
  font-weight: 600;
  color: #2c3e50;
}
</style>