Nested Market Map in JavaScript

Using ApexCharts with JavaScript

This Nested Market Map 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().

JavaScript installation guide
// A market map: every company is a tile, sized by market value and coloured by
// how its day went. Three levels, sector > industry > company, so the sectors
// read as blocks before you look at any single name.
//
// The figures are synthetic. A seeded generator (no Math.random, so the chart
// is stable across re-renders and SSR) draws caps from a heavy-tailed
// distribution and daily moves from a per-sector drift, which is what gives a
// real market map its handful of giants and long tail of small names.
// Shared by the vanilla-js / React / Vue builds.
var TAXONOMY = [
  {
    sector: 'Information Technology',
    weight: 30,
    drift: 0.9,
    industries: [
      { name: 'Semiconductors', n: 14, weight: 12 },
      { name: 'Software', n: 16, weight: 11 },
      { name: 'Hardware & Devices', n: 9, weight: 7 },
    ],
  },
  {
    sector: 'Financials',
    weight: 15,
    drift: -0.3,
    industries: [
      { name: 'Banks', n: 15, weight: 6 },
      { name: 'Insurance', n: 11, weight: 4 },
      { name: 'Capital Markets', n: 10, weight: 5 },
    ],
  },
  {
    sector: 'Health Care',
    weight: 14,
    drift: 0.2,
    industries: [
      { name: 'Pharmaceuticals', n: 12, weight: 6 },
      { name: 'Biotechnology', n: 14, weight: 4 },
      { name: 'Medical Devices', n: 10, weight: 4 },
    ],
  },
  {
    sector: 'Consumer Discretionary',
    weight: 11,
    drift: -0.6,
    industries: [
      { name: 'Retail', n: 13, weight: 5 },
      { name: 'Automobiles', n: 8, weight: 4 },
      { name: 'Hotels & Leisure', n: 9, weight: 2 },
    ],
  },
  {
    sector: 'Communication Services',
    weight: 9,
    drift: 1.4,
    industries: [
      { name: 'Interactive Media', n: 9, weight: 5 },
      { name: 'Telecom', n: 8, weight: 2 },
      { name: 'Entertainment', n: 9, weight: 2 },
    ],
  },
  {
    sector: 'Industrials',
    weight: 8,
    drift: 0.1,
    industries: [
      { name: 'Aerospace & Defence', n: 8, weight: 3 },
      { name: 'Machinery', n: 10, weight: 3 },
      { name: 'Transport', n: 9, weight: 2 },
    ],
  },
  {
    sector: 'Consumer Staples',
    weight: 6,
    drift: 0.4,
    industries: [
      { name: 'Food & Beverage', n: 11, weight: 3 },
      { name: 'Household Products', n: 8, weight: 3 },
    ],
  },
  {
    sector: 'Energy',
    weight: 5,
    drift: -1.7,
    industries: [
      { name: 'Oil & Gas', n: 12, weight: 4 },
      { name: 'Renewables', n: 8, weight: 1 },
    ],
  },
  {
    sector: 'Utilities',
    weight: 4,
    drift: -0.2,
    industries: [
      { name: 'Electric Utilities', n: 9, weight: 3 },
      { name: 'Water & Gas', n: 6, weight: 1 },
    ],
  },
  {
    sector: 'Real Estate',
    weight: 4,
    drift: -0.9,
    industries: [
      { name: 'REITs', n: 11, weight: 3 },
      { name: 'Property Services', n: 6, weight: 1 },
    ],
  },
  {
    sector: 'Materials',
    weight: 4,
    drift: 0.6,
    industries: [
      { name: 'Chemicals', n: 9, weight: 2 },
      { name: 'Metals & Mining', n: 9, weight: 2 },
    ],
  },
]

// Deterministic uniform stream (LCG), so every render draws the same market.
function rng(seed) {
  var s = seed >>> 0
  return function () {
    s = (s * 1664525 + 1013904223) >>> 0
    return s / 4294967296
  }
}

// Sum of 6 uniforms, centred: a cheap normal-ish draw.
function gauss(u) {
  var g = 0
  for (var k = 0; k < 6; k++) g += u()
  return (g - 3) / Math.sqrt(0.5)
}

var LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
function ticker(u) {
  var out = ''
  var len = 3 + (u() > 0.65 ? 1 : 0)
  for (var i = 0; i < len; i++) {
    out += LETTERS.charAt(Math.floor(u() * 26))
  }
  return out
}

var seen = {}
var u = rng(20260812)

var marketSeries = [
  {
    name: 'Market',
    data: TAXONOMY.map(function (sec) {
      return {
        x: sec.sector,
        children: sec.industries.map(function (ind) {
          var companies = []
          for (var i = 0; i < ind.n; i++) {
            // Heavy tail: a few names carry most of the industry's value.
            var scale = Math.exp(gauss(u) * 0.85)
            var cap = Math.max(4, Math.round((ind.weight * 90 * scale) / ind.n))
            // The day's move: the sector drifts, the name adds its own noise.
            var change = Math.round((sec.drift + gauss(u) * 1.6) * 100) / 100
            var t = ticker(u)
            while (seen[t]) t = ticker(u)
            seen[t] = true
            companies.push({ x: t, y: cap, colorValue: change })
          }
          return { x: ind.name, children: companies }
        }),
      }
    }),
  },
]

function fmtCap(v) {
  return v >= 1000 ? '$' + (v / 1000).toFixed(2) + 'T' : '$' + v + 'B'
}
function fmtPct(v) {
  return (v > 0 ? '+' : '') + v.toFixed(2) + '%'
}

var options = {
  series: marketSeries,
  chart: {
    id: 'marketMap',
    type: 'treemap',
    height: 620,
    fontFamily: 'inherit',
    background: 'transparent',
    animations: {
      enabled: true,
      speed: 500,
    },
    toolbar: {
      show: false,
    },
  },
  theme: {
    mode: 'dark',
  },
  title: {
    // Kept to one line: the breadcrumb that appears once you zoom in sits in the
    // band between the title and the plot, and a subtitle would squeeze it out.
    text: "Market map by sector - area is market value, colour is the day's move",
    align: 'left',
    style: {
      fontSize: '14px',
      fontWeight: 600,
    },
  },
  legend: {
    show: true,
    position: 'bottom',
  },
  stroke: {
    width: 1,
    colors: ['#0f1115'],
  },
  dataLabels: {
    enabled: true,
    style: {
      fontSize: '11px',
      fontWeight: 600,
    },
  },
  tooltip: {
    enabled: true,
    custom: function ({ seriesIndex, dataPointIndex, w }) {
      var d = w.config.series[seriesIndex].data[dataPointIndex]
      var up = d.colorValue >= 0
      return (
        '<div class="apexcharts-tooltip-title" style="font-weight:600">' +
        d.x +
        '</div>' +
        '<div style="padding:6px 10px 8px">' +
        fmtCap(d.y) +
        ' &nbsp;<span style="color:' +
        (up ? '#3ddc84' : '#ff6b5e') +
        '">' +
        fmtPct(d.colorValue) +
        '</span></div>'
      )
    },
  },
  plotOptions: {
    treemap: {
      borderRadius: 2,
      dataLabels: {
        format: 'truncate',
      },
      colorScale: {
        // Colour by the day's move, not by market value.
        colorValue: 'colorValue',
        gradient: {
          colors: ['#c0392b', '#5b6570', '#1e9e57'],
          midpoint: 0,
        },
        gradientLegend: {
          enabled: true,
          width: '46%',
          thickness: 10,
          formatter: function (v) {
            return (v > 0 ? '+' : '') + v.toFixed(1) + '%'
          },
        },
      },
      // Click a sector or an industry to fill the canvas with it.
      zoom: {
        enabled: true,
      },
      parents: {
        padding: 3,
        tooltip: {
          formatter: function (o) {
            return (
              '<div class="apexcharts-tooltip-title" style="font-weight:600">' +
              o.name +
              '</div>' +
              '<div style="padding:6px 10px 8px">' +
              fmtCap(o.value) +
              ' &nbsp;&middot;&nbsp; ' +
              o.leafCount +
              ' companies &nbsp;&middot;&nbsp; ' +
              o.percentOfTotal.toFixed(1) +
              '% of market</div>'
            )
          },
        },
      },
      levels: [
        {
          // Sector.
          padding: 5,
          header: {
            height: 26,
            minWidth: 64,
            style: {
              fontSize: '13px',
              fontWeight: 700,
            },
            formatter: function (name, o) {
              return name + '   ' + fmtCap(o.value)
            },
          },
        },
        {
          // Industry.
          padding: 2,
          header: {
            height: 15,
            minWidth: 46,
            style: {
              fontSize: '10px',
              fontWeight: 500,
            },
          },
        },
      ],
    },
  },
}

var chart = new ApexCharts(document.querySelector('#chart'), options)
chart.render()