Donut and Polar Area Charts with One Device Highlighted in React
Using ApexCharts with React
This Donut and Polar Area Charts with One Device Highlighted example wires ApexCharts into React through the react-apexcharts component.
Install it with npm install react-apexcharts apexcharts, then mount the chart with <Chart type="..." options={options} series={series} />.
import React from 'react'
import ReactApexChart from 'react-apexcharts'
import ApexCharts from 'apexcharts'
import './styles.css'
import 'apexcharts/features/highlight-filter'
// Shared by the vanilla-js, React and Vue builds.
//
// Every channel's figure is the whole and one device's is the part. A pick
// is an ordinary update of highlightFilter.data, one value per slice; null
// clears it.
var CHANNELS = ['Organic', 'Paid search', 'Social', 'Email', 'Referral']
// Sums: a channel's sessions and orders are its devices' added up, so a
// device's part always fits inside the channel's slice.
var SESSIONS = [4200, 2600, 1900, 1100, 700]
var SESSIONS_BY_DEVICE = {
mobile: [2500, 1300, 1500, 400, 300],
desktop: [1400, 1100, 300, 600, 330],
tablet: [300, 200, 100, 100, 70],
}
var ORDERS = [300, 240, 160, 180, 120]
var ORDERS_BY_DEVICE = {
mobile: [140, 100, 100, 60, 45],
desktop: [130, 120, 45, 100, 60],
tablet: [30, 20, 15, 20, 15],
}
// Averages: a channel's average order value is its devices' revenue over
// their orders, so a device that spends more per order sits above it.
var ORDER_VALUE = [66, 85, 49, 76, 95]
var ORDER_VALUE_BY_DEVICE = {
mobile: [52, 70, 41, 60, 76],
desktop: [81, 98, 66, 84, 109],
tablet: [70, 85, 55, 80, 95],
}
function pickDevice(device) {
document.querySelectorAll('[data-device]').forEach(function (b) {
b.classList.toggle('active', b.getAttribute('data-device') === device)
})
// A pick is an ordinary update; null clears it.
ApexCharts.getChartByID('sessions').updateOptions({
highlightFilter: { data: device ? SESSIONS_BY_DEVICE[device] : null },
})
ApexCharts.getChartByID('orders').updateOptions({
highlightFilter: { data: device ? ORDERS_BY_DEVICE[device] : null },
})
ApexCharts.getChartByID('order-value').updateOptions({
highlightFilter: { data: device ? ORDER_VALUE_BY_DEVICE[device] : null },
})
}
function wireDevices() {
document.querySelectorAll('[data-device]').forEach(function (b) {
b.addEventListener('click', function () {
pickDevice(b.getAttribute('data-device'))
})
})
}
const ApexChart = () => {
const [state, setState] = React.useState({
series: SESSIONS,
options: {
chart: {
id: 'sessions',
type: 'donut',
height: 360,
toolbar: { show: false },
},
labels: CHANNELS,
colors: ['#0284c7', '#0f766e', '#f59e0b', '#e11d48', '#64748b'],
plotOptions: {
pie: {
donut: {
size: '58%',
labels: {
show: true,
value: {
formatter: function (v) {
return Number(v).toLocaleString()
},
},
total: {
show: true,
label: 'Sessions',
formatter: function (w) {
return w.globals.seriesTotals
.reduce(function (a, b) {
return a + b
}, 0)
.toLocaleString()
},
},
},
},
},
},
// No slice borders: the faded tints alone keep the slices apart.
stroke: { width: 0 },
dataLabels: { enabled: true },
legend: { position: 'bottom' },
title: { text: 'Sessions by channel', style: { fontSize: '13px' } },
tooltip: {
y: {
formatter: function (v) {
return Number(v).toLocaleString()
},
},
},
highlightFilter: { data: SESSIONS_BY_DEVICE.mobile },
},
series1: ORDERS,
options1: {
chart: {
id: 'orders',
type: 'polarArea',
height: 360,
toolbar: { show: false },
},
labels: CHANNELS,
colors: ['#0284c7', '#0f766e', '#f59e0b', '#e11d48', '#64748b'],
fill: { opacity: 0.85 },
stroke: { width: 0 },
legend: { position: 'bottom' },
title: { text: 'Orders by channel', style: { fontSize: '13px' } },
highlightFilter: { data: ORDERS_BY_DEVICE.mobile },
},
series2: ORDER_VALUE,
options2: {
chart: {
id: 'order-value',
type: 'polarArea',
height: 360,
toolbar: { show: false },
},
labels: CHANNELS,
colors: ['#0284c7', '#0f766e', '#f59e0b', '#e11d48', '#64748b'],
fill: { opacity: 0.85 },
stroke: { width: 0 },
yaxis: {
labels: {
formatter: function (v) {
return '$' + v
},
},
},
legend: { position: 'bottom' },
title: {
text: 'Average order value by channel',
style: { fontSize: '13px' },
},
tooltip: {
y: {
formatter: function (v) {
return '$' + v
},
},
},
highlightFilter: { data: ORDER_VALUE_BY_DEVICE.mobile },
},
})
React.useEffect(() => {
// The react-apexcharts wrappers own the renders, so reach the live
// instances by their chart.id. (pickDevice lives in the shared head script.)
const timer = window.setInterval(() => {
if (
!ApexCharts.getChartByID('sessions') ||
!ApexCharts.getChartByID('orders') ||
!ApexCharts.getChartByID('order-value')
)
return
window.clearInterval(timer)
wireDevices()
}, 50)
return () => window.clearInterval(timer)
}, [])
return (
<div>
<div className="wrap">
<h1>Which device brings the traffic, and the bigger orders?</h1>
<p>
Pick a device. Each channel stays faded and the device's share is
drawn solid in front.
</p>
<div className="actions">
<span>Device</span>
<button data-device="mobile" className="active">
Mobile
</button>
<button data-device="desktop">Desktop</button>
<button data-device="tablet">Tablet</button>
<button data-device="">All (clear)</button>
</div>
<h2>
<small>Sums</small>The device is a share of the channel
</h2>
<p>
A channel's sessions and orders are its devices' added up, so the
solid part always fits inside its slice.
</p>
<div className="grid">
<div className="card">
<div id="chart">
<ReactApexChart
options={state.options}
series={state.series}
type="donut"
height={360}
/>
</div>
</div>
<div className="card">
<div id="chart2">
<ReactApexChart
options={state.options1}
series={state.series1}
type="polarArea"
height={360}
/>
</div>
</div>
</div>
<h2>
<small>Averages</small>A device can spend more than its channel
</h2>
<p>
An average is not a sum, so a device can sit above its channel. Pick
Desktop: each solid slice runs past the channel's value, marked by a
dashed line.
</p>
<div className="grid">
<div className="card">
<div id="chart3">
<ReactApexChart
options={state.options2}
series={state.series2}
type="polarArea"
height={360}
/>
</div>
</div>
</div>
</div>
</div>
)
}
export default ApexChart