Average Profit Bar Chart with One Product Line Highlighted in Vue
Using ApexCharts with Vue
This Average Profit Bar Chart with One Product Line Highlighted 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" />.
<template>
<div>
<div class="wrap">
<h1>Averages can outgrow the whole</h1>
<p>
Each bar is a store's average profit per order across every product.
Pick a product line to draw that line's own average solid in front.
Where it beats the overall average, the axis stretches to fit it and a
dashed edge marks where the overall average ends; where it loses money,
it is drawn on its own side of zero.
</p>
<div class="actions">
<span>Product line</span>
<button data-line="premium" class="active">Premium</button>
<button data-line="budget">Budget</button>
<button data-line="">All (clear)</button>
</div>
<div class="chart-wrap">
<div id="chart">
<apexchart
type="bar"
height="360"
:options="chartOptions"
:series="series"
></apexchart>
</div>
</div>
</div>
</div>
</template>
<script>
import VueApexCharts from 'vue-apexcharts'
import ApexCharts from 'apexcharts'
import 'apexcharts/features/highlight-filter'
// Shared by the vanilla-js, React and Vue builds.
function money(v) {
return (v < 0 ? '-$' : '$') + Math.abs(Math.round(v))
}
//
// Average profit per order by store, across all products (the wholes), and the
// same average over one product line (the parts). An average over a subset
// can be larger than the overall one, or fall on the other side of zero, so
// the parts here do both.
var STORES = ['Seattle', 'Denver', 'Austin', 'Chicago', 'Boston', 'Miami']
var ALL = [42, -18, 31, 24, 12, -9]
var LINES = {
premium: [
{ x: 'Seattle', y: 42, highlight: 61 },
{ x: 'Denver', y: -18, highlight: -4 },
{ x: 'Austin', y: 31, highlight: 48 },
{ x: 'Chicago', y: 24, highlight: 19 },
{ x: 'Boston', y: 12, highlight: 27 },
{ x: 'Miami', y: -9, highlight: 6 },
],
budget: [
{ x: 'Seattle', y: 42, highlight: 18 },
{ x: 'Denver', y: -18, highlight: -26 },
{ x: 'Austin', y: 31, highlight: 12 },
{ x: 'Chicago', y: 24, highlight: 28 },
{ x: 'Boston', y: 12, highlight: -3 },
{ x: 'Miami', y: -9, highlight: -21 },
],
}
function seriesFor(line) {
return [
{
name: 'Avg profit per order',
data: line
? LINES[line]
: STORES.map(function (s, i) {
return { x: s, y: ALL[i] }
}),
},
]
}
function pickLine(chart, line) {
document.querySelectorAll('[data-line]').forEach(function (b) {
b.classList.toggle('active', b.getAttribute('data-line') === line)
})
return chart.updateSeries(seriesFor(line))
}
function wireLines(chart) {
document.querySelectorAll('[data-line]').forEach(function (b) {
b.addEventListener('click', function () {
pickLine(chart, b.getAttribute('data-line'))
})
})
}
export default {
components: {
apexchart: VueApexCharts,
},
data: function () {
return {
series: seriesFor('premium'),
chartOptions: {
chart: {
id: 'profit',
type: 'bar',
height: 360,
toolbar: { show: false },
},
colors: ['#0d9488'],
plotOptions: {
bar: { horizontal: true, barHeight: '62%', borderRadius: 4 },
},
dataLabels: {
enabled: true,
formatter: money,
},
xaxis: {
labels: { formatter: money },
},
grid: {
xaxis: { lines: { show: true } },
yaxis: { lines: { show: false } },
},
tooltip: {
y: { formatter: money },
},
},
lineTimer: null,
}
},
mounted: function () {
// The vue-apexcharts wrapper owns the render, so reach the live instance by
// its chart.id. (seriesFor / pickLine live in the shared head script.)
var me = this
me.lineTimer = window.setInterval(function () {
var chart = ApexCharts.getChartByID('profit')
if (!chart) return
window.clearInterval(me.lineTimer)
wireLines(chart)
}, 50)
},
beforeDestroy: function () {
window.clearInterval(this.lineTimer)
},
}
</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);
}
.actions {
margin: 0 0 14px;
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.actions span {
font-size: 13px;
color: #5b6b78;
margin-right: 4px;
}
.actions button {
color: #2c3e50;
background: #fff;
border: 1px solid #cfd8e3;
padding: 6px 14px;
font-weight: 600;
font-size: 13px;
border-radius: 6px;
cursor: pointer;
}
.actions button.active {
color: #fff;
background: #0d9488;
border-color: #0d9488;
}
</style>