Web Components Community 🔷

Cover image for Fixing the Donut
Dominic Myers
Dominic Myers

Posted on

Fixing the Donut

(cover 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 WCPieChart extends HTMLElement {
  static get observedAttributes() {
    return ['width', 'duration', 'delay', 'thickness']
  }
  get style() {
    return `
      <style>
        :host {
          --width: ${this.width} 
        }
        * {
          box-sizing: border-box;
        }
        div {
          width: calc(var(--width)*1px);
          height: calc(var(--width)*1px);
          position: relative;
        }
      </style>`
  }
  constructor() {
    super()
    this.shadow = this.attachShadow({
      mode: 'closed',
    })
    this.shadow.innerHTML = `${this.style}<div></div>`
    try {
      window.CSS.registerProperty({
        name: '--p',
        syntax: '<number>',
        inherits: true,
        initialValue: 0,
      })
    } catch (e) {
      if (e.name === 'InvalidModificationError') {
        return // We've already added it before!
      } else {
        console.warn(
          'Browser does not support animated conical gradients',
          e.name
        )
      }
    }
  }
  render() {
    if (Array.isArray(this.segments)) {
      const segments = [...this.segments]
      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
        this.div.append(segment)
      })
    }
  }
  connectedCallback() {
    this.div = this.shadow.querySelector('div')
    this.segments = [...this.querySelectorAll('wc-pie-slice')]
    this.render()
  }
  attributeChangedCallback(name, oldValue, newValue) {
    if (newValue !== oldValue) {
      this.shadow.innerHTML = `${this.style}<div></div>`
      this.div = this.shadow.querySelector('div')
      this.render()
    }
  }
  get width() {
    return Number(this.getAttribute('width')) || 150
  }
  get duration() {
    return Number(this.getAttribute('duration')) || 2000
  }
  get delay() {
    return Number(this.getAttribute('delay')) || 500
  }
  get thickness() {
    return Number(this.getAttribute('thickness')) || 0.2
  }
}

class WCPieSlice extends HTMLElement {
  static get observedAttributes() {
    return ['width', 'duration', 'delay', 'color', 'thickness', 'rotate']
  }
  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.specifiedColour} 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, transparent 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.specifiedColour = this.color
  }
  render() {
    this.shadow.innerHTML = `${this.style}<div></div>`
  }
  connectedCallback() {
    this.render()
  }
  attributeChangedCallback(name, oldValue, newValue) {
    this.render()
  }
  get width() {
    return Number(this.getAttribute('width'))
  }
  get duration() {
    return Number(this.getAttribute('duration'))
  }
  get delay() {
    return Number(this.getAttribute('delay'))
  }
  get color() {
    return this.getAttribute('color') || this.getRandomColor
  }
  get thickness() {
    return Number(this.getAttribute('thickness'))
  }
  get rotate() {
    return Number(this.getAttribute('rotate'))
  }
  get end() {
    return Number(this.getAttribute('end'))
  }
  get getRandomColor() {
    const letters = '0123456789ABCDEF'
    let color = '#'
    for (let i = 0; i < 6; i++) {
      color += letters[Math.floor(Math.random() * 16)]
    }
    return color
  }
}

window.customElements.define('wc-pie-chart', WCPieChart)
window.customElements.define('wc-pie-slice', WCPieSlice)
Enter fullscreen mode Exit fullscreen mode

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.

Should you want to give it a go I've published it on npm, please do give it a spin and let me know if things are broken!

There's a demo on GitHub where you can see that the component can be manipulated from outside, and even added dynamically - which is something I though was a brilliant idea from the original component.

From the README:

<wc-pie-chart id="existing"
              thickness="0.1">
  <wc-pie-slice value="5"
                color="#E64C65"/>
  <wc-pie-slice value="5"
                color="#11A8AB"/>
  <wc-pie-slice value="5"
                color="#394264"/>
</wc-pie-chart>
<script type="module"
        src="https://unpkg.com/wc-pie-chart/wc-pie-chart.js"></script>
Enter fullscreen mode Exit fullscreen mode

If anyone can help get it so I can pop a demo into the listing on
WEBCOMPONENTS.ORG, I'd be really grateful.

Top comments (5)

Collapse
 
berrry profile image
berry sertty

IntellectualsDen is one of the best working and legit companies that provide academic writing services to the students. An academic writing company can only succeed in the industry if it produces quality work. Nowadays there are many companies ghost writing for the clients.
scamsoldier.com/review/intellectua...

Collapse
 
sammi profile image
sammi

ReviewersHut provides legitimate and verified reviews from 100s of reliable users, where customer reviews and ratings so that you can save time and make decisions.

Collapse
 
sammi profile image
sammi

Academic paper pros reviews are perfectionists and provide proactive academic writing services.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.