City Bubbles in JavaScript
Using ApexCharts with JavaScript
This City Bubbles example uses ApexCharts.js directly in JavaScript, with no wrapper component.
Install it with npm install apexcharts, then mount the chart with new ApexCharts(element, options).render().
// 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.',
},
}
var options = {
series: seriesFor('population'),
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
)
},
},
},
},
}
var chart = new ApexCharts(document.querySelector('#chart'), options)
chart.render()
// Button-driven size-metric switch: each view is a chart.updateSeries payload
// that changes only the bubble size field, so every bubble tweens its radius.
// (REGIONS, DATA, seriesFor, VIEWS live in the shared head script.)
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