<template>
  <div>
    <div class="wrap">
      <h1>Earnings versus the cost of living</h1>
      <p class="lead">
        Each bubble is a city: average income across the bottom, cost of living
        up the side, sized by population.
      </p>

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

      <div class="actions" id="size-nav">
        <button data-size="population" class="active">
          Size by population
        </button>
        <button data-size="gdp">Size by GDP</button>
      </div>

      <p class="caption" id="caption"></p>

      <div class="note">
        <code>plotOptions.unit.scatter.y: 'value'</code> switches the scatter
        layout to a 2D value-value plot: each datum's own <code>x</code> and
        <code>y</code> on two drawn numeric axes, coloured by category.
        <code>scatter.sizeRange</code>
        turns the dots into bubbles scaled by area from each datum's
        <code>sizeField</code> (here <code>z</code>). Switching the size metric
        is a plain <code>chart.updateSeries()</code>, so every bubble tweens its
        radius; hiding a region from the legend fades its bubbles out in place.
        The unit chart is a premium type; without a license it renders with a
        trial watermark. Figures are approximate.
      </div>
    </div>
  </div>
</template>

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

// A 2D bubble scatter: each bubble is a city, placed by average income (x) and
// cost of living (y), sized by a third field and coloured by region. This is
// the unit chart's `layout: 'scatter'` in `y: 'value'` mode (each datum's own x
// + y on two numeric axes) with `sizeRange` turning the dots into area-scaled
// bubbles.
//
// The size toggle is the story: by POPULATION the megacities (Tokyo, Delhi,
// Shanghai, Sao Paulo) swell; by GDP the money centres (Tokyo, New York, London,
// Paris) lead instead. Every bubble tweens its size. Figures are approximate
// (public-data style). Shared by the vanilla-js / React / Vue builds.
var REGIONS = [
  { name: 'Americas', color: '#22C55E' },
  { name: 'Europe', color: '#0EA5E9' },
  { name: 'Asia', color: '#F59E0B' },
  { name: 'Oceania', color: '#F43F5E' },
]

// [city, avg income (US$/yr), cost of living (index, NYC = 100),
//  metro population (millions), metro GDP (US$ billions)]
var DATA = {
  Americas: [
    ['New York', 55000, 100, 18.9, 2000],
    ['San Francisco', 62000, 98, 4.7, 650],
    ['Toronto', 40000, 72, 6.4, 400],
    ['Sao Paulo', 12000, 40, 22.6, 480],
    ['Mexico City', 11000, 38, 22.0, 480],
  ],
  Europe: [
    ['Zurich', 72000, 118, 1.4, 340],
    ['London', 45000, 85, 14.3, 1000],
    ['Paris', 38000, 80, 11.1, 850],
    ['Berlin', 36000, 70, 6.1, 180],
    ['Moscow', 18000, 45, 12.6, 550],
  ],
  Asia: [
    ['Tokyo', 40000, 83, 37.4, 1600],
    ['Singapore', 55000, 95, 5.9, 470],
    ['Dubai', 45000, 70, 3.5, 130],
    ['Hong Kong', 42000, 90, 7.5, 380],
    ['Seoul', 34000, 78, 26.0, 780],
    ['Shanghai', 20000, 60, 27.0, 680],
    ['Mumbai', 8000, 35, 20.7, 310],
    ['Delhi', 7000, 32, 32.0, 370],
  ],
  Oceania: [
    ['Sydney', 48000, 88, 5.3, 400],
    ['Melbourne', 44000, 82, 5.0, 250],
  ],
}

// z = the bubble size field: 'population' (people) or 'gdp' (metro output).
function seriesFor(sizeBy) {
  return REGIONS.map(function (r) {
    return {
      name: r.name,
      data: DATA[r.name].map(function (c) {
        return {
          name: c[0],
          x: c[1],
          y: c[2],
          z: sizeBy === 'gdp' ? c[4] : c[3],
        }
      }),
    }
  })
}

var VIEWS = {
  population: {
    caption:
      'Sized by population, the megacities swell: Tokyo, Delhi, Shanghai and Sao Paulo dominate, even where incomes are low and living is cheap.',
  },
  gdp: {
    caption:
      'Sized by economic output, the money centres take over: New York, Tokyo, London and Paris lead, while populous but poorer cities shrink.',
  },
}

export default {
components: {
apexchart: VueApexCharts,
},
data: function () {
return {
series: seriesFor('population'),
chartOptions: {
chart: {
  id: 'cityBubbles',
  type: 'unit',
  height: 520,
  animations: {
    enabled: true,
    speed: 700,
  },
},
colors: REGIONS.map(function (r) {
  return r.color
}),
// Translucent fills so overlapping bubbles read through each other.
fill: {
  opacity: 0.7,
},
legend: {
  position: 'bottom',
},
plotOptions: {
  unit: {
    layout: 'scatter',
    scatter: {
      y: 'value',
      xMin: 0,
      yMin: 0,
      xTitle: 'Average income (US$/yr)',
      yTitle: 'Cost of living (NYC = 100)',
      xFormatter: function (v) {
        return '$' + Math.round(v / 1000) + 'k'
      },
      sizeRange: [7, 44],
    },
    tooltip: {
      formatter: function (o) {
        var d = (o && o.datum) || {}
        if (!d.name) return ''
        return (
          d.name +
          ': $' +
          Math.round(d.x / 1000) +
          'k income, cost of living ' +
          d.y
        )
      },
    },
  },
},
},
bubbleTimer: null,
}
},
mounted: function () {
  // Reach the live instance by its chart.id, then wire the buttons. (Data lives
  // in the shared head script.) updateSeries drives the instance directly, so no
  // reactive data changes under it.
  var me = this
  me.bubbleTimer = window.setInterval(function () {
    var chart = ApexCharts.getChartByID('cityBubbles')
    if (!chart) return
    window.clearInterval(me.bubbleTimer)

    var buttons = document.querySelectorAll('#size-nav button')
    var caption = document.getElementById('caption')

    function showSize(key) {
      buttons.forEach(function (b) {
        b.classList.toggle('active', b.getAttribute('data-size') === key)
      })
      caption.textContent = VIEWS[key].caption
      chart.updateSeries(seriesFor(key))
    }

    buttons.forEach(function (btn) {
      btn.addEventListener('click', function () {
        showSize(btn.getAttribute('data-size'))
      })
    })

    caption.textContent = VIEWS.population.caption
  }, 50)
},
beforeDestroy: function () {
  window.clearInterval(this.bubbleTimer)
},,
}
</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: 800px;
  margin: 0 auto;
}
h1 {
  font-size: 22px;
  margin: 0 0 8px;
}
.lead {
  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);
}
.actions {
  margin: 20px auto 10px;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  justify-content: center;
}
.actions button {
  color: #fff;
  background: #94a3b8;
  border: none;
  padding: 8px 18px;
  font-weight: 600;
  font-size: 13px;
  border-radius: 6px;
  cursor: pointer;
}
.actions button.active {
  background: #4338ca;
}
.caption {
  text-align: center;
  color: #5b6b78;
  font-size: 13px;
  line-height: 1.6;
  max-width: 620px;
  margin: 0 auto;
  min-height: 34px;
}
.note {
  background: #eef2ff;
  border-left: 3px solid #4338ca;
  padding: 12px 16px;
  font-size: 13px;
  color: #333;
  border-radius: 2px;
  line-height: 1.6;
  margin: 26px auto 0;
}
.note code {
  background: #e0e7ff;
  padding: 1px 5px;
  border-radius: 3px;
}
</style>
City Bubbles - Vue Unit Charts | ApexCharts.js | ApexCharts.js