Image by Jessica May
I've been playing with a pie-chart web component for a little while now, and I was thrilled with the result... until I started to use it in anger, that is.
The thing was, angles over 180 messed something up - I decided to revisit it this week after reading this article by Temani Afif, and I'm once again thrilled to see it working - and working correctly (at least in Chrome and Edge):
class DMPieChart extends HTMLElement {
static get observedAttributes() {
return [
'width', // width of the chart
'duration', // duration of the animation
'delay', // delay before the animation
'thickness' // thickness of the slices
];
}
get style() {
return `
<style>
* {
box-sizing: border-box;
}
div {
width: ${this.width}px;
height: ${this.width}px;
position: relative;
}
</style>
`
}
constructor() {
super()
this.shadow = this.attachShadow({
mode: 'closed'
})
this.shadow.innerHTML = `
${this.style}
<div>
<slot name='pie-slices'></slot>
</div>
`
try {
window.CSS.registerProperty({
name: '--p',
syntax: '<number>',
inherits: true,
initialValue: 0,
})
}catch (e) {
console.warn('Browser does not support animated conical gradients')
}
}
connectedCallback() {
const segments = [...this.querySelectorAll('dm-pie-slice')]
const total = segments.reduce((p, c) => p + Number(c.getAttribute('value')), 0)
let durationTotal = this.delay;
let rotationTotal = 0
const totalDegree = 360/total
segments.forEach(segment => {
const value = Number(segment.getAttribute('value'))
const currentRotation = totalDegree * value
const animationDuration = currentRotation / (360/Number(this.duration))
segment.setAttribute('thickness', this.thickness * this.width)
segment.setAttribute('end', (value / total) * 100)
segment.setAttribute('rotate', rotationTotal)
segment.setAttribute('delay', durationTotal)
segment.setAttribute('duration', animationDuration)
segment.setAttribute('width', this.width)
rotationTotal += currentRotation
durationTotal += animationDuration
})
}
get width() {
return Number(this.getAttribute('width')) || 150 // 150px by default
}
get duration() {
return Number(this.getAttribute('duration')) || 5000 // 5 seconds by default
}
get delay() {
return Number(this.getAttribute('delay')) || 500 // half a second by default
}
get thickness() {
return Number(this.getAttribute('thickness')) || 0.2 // 60% of width by default
}
}
class DMPieSlice extends HTMLElement{
static get observedAttributes() {
return [
'width', // width of the chart
'duration', // duration of the animation
'delay', // delay before the animation
'color', // color of the arc
'thickness', // thickness of the arc
'rotate' // how far to rotate the arc
];
}
get style() {
return `
<style>
* {
box-sizing: border-box;
}
div {
--p: ${this.end};
width: ${this.width}px;
aspect-ratio: 1;
margin: 0;
position: absolute;
left: 50%;
top: 50%;
animation-name: p;
animation-fill-mode: both;
animation-timing-function: ease-in-out;
transform: translate(-50%, -50%);
animation-duration: ${this.duration}ms;
animation-delay: ${this.delay}ms;
}
div:before {
content: "";
position: absolute;
border-radius: 50%;
inset: 0;
background: conic-gradient(from ${this.rotate}deg, ${this.color} calc(var(--p)*1%), transparent 0);
-webkit-mask: radial-gradient(farthest-side, transparent calc(99% - ${this.thickness}px), #000 calc(100% - ${this.thickness}px));
mask: radial-gradient(farthest-side,#0000 calc(99% - ${this.thickness}px), #000 calc(100% - ${this.thickness}px));
}
@keyframes p {
from {
--p: 0
}
}
</style>
`
}
constructor() {
super();
this.shadow = this.attachShadow({
mode: 'closed'
})
this.shadow.innerHTML = `${this.style}<div/>`
}
get width() {
return Number(this.getAttribute('width')) || 150 // 150px by default
}
get duration() {
return Number(this.getAttribute('duration'))
}
get delay() {
return Number(this.getAttribute('delay'))
}
get color() {
return this.getAttribute('color') || '#000000' // black by default
}
get thickness() {
return Number(this.getAttribute('thickness'))
}
get rotate() {
return Number(this.getAttribute('rotate'))
}
get end() {
return Number(this.getAttribute('end'))
}
}
window.customElements.define('dm-pie-chart', DMPieChart)
window.customElements.define('dm-pie-slice', DMPieSlice)
It is significantly cleaner but only animates within Chrome and Edge (everywhere else, it should just appear without animation, which is a shame).
I've also moved away from using the CSSStyleSheet interface as Safari throws a wobble when it's used without a polyfill, which is a shame as constructible/adoptable style sheets rock!
The animation is thanks to @property CSS at-rule. It is utterly brilliant, especially once I understood you could inject it via JS, as injecting it into the component's CSS didn't work at all - either via the static CSS or the constructible/adoptable CSS.
Bundling it into one file should also ease its adoption.
Top comments (0)