<template>
  <div>
    <div class="panel">
      <div class="legend">
        <span class="item"><span class="dot hollow"></span> 2019</span>
        <span class="item"><span class="dot filled"></span> 2024</span>
      </div>
    </div>

    <div id="chart">
      <apexchart
        type="dumbbell"
        height="440"
        :options="chartOptions"
        :series="series"
      ></apexchart>
    </div>

    <div class="panel">
      <div class="note">
        There is no built-in dumbbell type here. It is a custom series
        registered with
        <code
          >ApexCharts.registerSeriesType('dumbbell', &#123; dataType: 'rangeXY',
          renderItem &#125;)</code
        >. <code>renderItem</code> draws one stem and two circles per datum
        through the primitive <code>api</code>, and
        <code>dataType: 'rangeXY'</code> routes the
        <code>y: [start, end]</code> pair through the range path so the y-axis
        fits both bounds and the tooltip reads "start - end". Hover a marker to
        see it.
      </div>
    </div>
  </div>
</template>

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

// Marks (#11): register a reusable "dumbbell" series type once. Each datum is
// a { x, y: [start, end] } pair. dataType 'rangeXY' tells ApexCharts to fold
// BOTH y-bounds into the axis scale (so the axis fits the full range, not just
// the end value) and to render the tooltip as "start - end".
ApexCharts.registerSeriesType('dumbbell', {
  dataType: 'rangeXY',
  renderItem: function (ctx) {
    var datum = ctx.datum
    var x = ctx.x
    var scales = ctx.scales
    var api = ctx.api
    var yStart = scales.y(datum.y[0])
    var yEnd = scales.y(datum.y[1])
    // connecting stem
    api.line({
      x1: x,
      y1: yStart,
      x2: x,
      y2: yEnd,
      stroke: '#d0d5dd',
      width: 4,
      lineCap: 'round',
    })
    // start marker (hollow ring) and end marker (filled)
    api.circle({
      cx: x,
      cy: yStart,
      r: 7,
      fill: '#fff',
      stroke: '#008FFB',
      strokeWidth: 3,
    })
    api.circle({ cx: x, cy: yEnd, r: 7, fill: '#00B894' })
  },
})

var dumbbellData = [
  { x: 'Austin', y: [312, 498] },
  { x: 'Denver', y: [398, 545] },
  { x: 'Miami', y: [285, 610] },
  { x: 'Seattle', y: [512, 720] },
  { x: 'Boise', y: [255, 415] },
  { x: 'Raleigh', y: [268, 402] },
  { x: 'Phoenix', y: [295, 468] },
]

export default {
  components: {
    apexchart: VueApexCharts,
  },
  data: function () {
    return {
      series: [{ name: 'Price range', data: dumbbellData }],
      chartOptions: {
        chart: {
          height: 440,
          type: 'dumbbell',
          animations: { enabled: false },
          toolbar: { show: false },
        },
        grid: {
          padding: { left: 15, right: 15 },
          xaxis: { lines: { show: true } },
        },
        title: {
          text: 'Median home price by city: 2019 vs 2024',
          align: 'left',
        },
        subtitle: {
          text: 'A custom "dumbbell" series type built with Marks (registerSeriesType)',
          align: 'left',
        },
        xaxis: {
          type: 'category',
          tooltip: { enabled: false },
        },
        yaxis: {
          min: 200,
          tickAmount: 6,
          labels: {
            formatter: function (val) {
              return '$' + Math.round(val) + 'k'
            },
          },
        },
        tooltip: {
          y: {
            formatter: function (val) {
              return '$' + val + 'k'
            },
          },
        },
      },
    }
  },
}
</script>

<style>
#chart {
  max-width: 780px;
  margin: 20px auto;
}
.panel {
  max-width: 780px;
  margin: 16px auto;
  font-family: sans-serif;
}
.legend {
  display: flex;
  gap: 22px;
  align-items: center;
  font-size: 13px;
  color: #334;
  margin: 4px 4px 0;
}
.legend .item {
  display: flex;
  gap: 7px;
  align-items: center;
}
.legend .dot {
  width: 14px;
  height: 14px;
  border-radius: 50%;
}
.legend .hollow {
  background: #fff;
  border: 3px solid #008ffb;
  box-sizing: border-box;
}
.legend .filled {
  background: #00b894;
}
.note {
  background: #f4f8ff;
  border-left: 3px solid #5b8cff;
  padding: 10px 14px;
  font-size: 13px;
  color: #234;
  margin-top: 12px;
  border-radius: 2px;
  line-height: 1.55;
}
.note code {
  background: #e6eeff;
  padding: 1px 5px;
  border-radius: 3px;
}
</style>
Dumbbell (Custom Series) - Vue Custom Series | ApexCharts.js | ApexCharts.js