Pie, Donut and Polar Area in React
Using ApexCharts with React
This Pie, Donut and Polar Area 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