DEV Community

Dominic Myers
Dominic Myers

Posted on • Originally published at drmsite.blogspot.com on

Fixing the Donut

By Jessica May via https://scop.io/collections/vendors?q=Jessica%20May
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)

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.

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read more

Top comments (0)

nextjs tutorial video

Youtube Tutorial Series

So you built a Next.js app, but you need a clear view of the entire operation flow to be able to identify performance bottlenecks before you launch. But how do you get started? Get the essentials on tracing for Next.js from @nikolovlazar in this video series 👀

Watch the Youtube series

👋 Kindness is contagious

Engage with a sea of insights in this enlightening article, highly esteemed within the encouraging DEV Community. Programmers of every skill level are invited to participate and enrich our shared knowledge.

A simple "thank you" can uplift someone's spirits. Express your appreciation in the comments section!

On DEV, sharing knowledge smooths our journey and strengthens our community bonds. Found this useful? A brief thank you to the author can mean a lot.

Okay