DEV Community

Cover image for What Web technologies are required to draw a Pie Chart in 2021? (spoiler alert: a standard Web Component will do) 🥧
Danny Engelman
Danny Engelman

Posted on • Edited on

What Web technologies are required to draw a Pie Chart in 2021? (spoiler alert: a standard Web Component will do) 🥧

🥧 TL;DR;


🥧 What Web technologies are required to draw a Pie Chart in 2021?


🥧 HTML, so many moons ago

Had you asked me the question, when I first learned HTML,

I would have written:

<pie-chart>
  <slice color="green">HTML 100%</slice>
</pie-chart>
Enter fullscreen mode Exit fullscreen mode

🥧 Required technologies in 2021

Alas, using any Charting Library, the modern truth answer is more like:

Compared to my early Web adventures, you almost have to be a rocket-scientist to get a Pie Chart in a Web Page; not to mention all the skills and tools required to start with a Page in the first place; and then those build steps...

     <PieChart
        data={[
                { title: 'HTML', value: 10, color: 'green' },
                { title: 'JavaScript', value: 75, color: 'red' },
                { title: 'CSS', value: 15, color: 'blue' },
        ]},
        radius={PieChart.defaultProps.radius - shiftSize}
        segmentsShift={(index) => (index === 0 ? shiftSize : 0.5)}
        label={({ dataEntry }) => dataEntry.value}
        labelStyle={{
          ...defaultLabelStyle,
        }}
      />
Enter fullscreen mode Exit fullscreen mode

🥧 HTML powered by a Web Component

In my past 28 Web years, I have used many Frameworks and Libraries, and payed the price multiple times for using technologies that eventually died.

Now the WHATWG, since 2019, is in complete control of the Web HTML standard, I more and more stick to standard technologies only.

Using modern W3C Standard Web Components my design today in 2021 is:

<pie-chart>
  <slice size="90" stroke="green">HTML</slice>
  <slice size="1"  stroke="red">JavaScript</slice>
  <slice size="9"  stroke="blue">CSS</slice>
</pie-chart>
Enter fullscreen mode Exit fullscreen mode


🥧 HTML is still great!

Mind you, I am slightly biased towards HTML because JavaScript and CSS did not exist when I started with Web Development.

HTML is the primary technology that made the Web great and HUGE.
Everyone with basic (WordPerfect) Word Processing skills could create Web pages in those days.
My retired mum did, my 6 year old niece did.

Everyone with basic HTML skills CAN create a Pie Chart in 2021

Modern Web Development does not have to be all about HTML-in-JS and CSS-in-JS; only Developers are comfortable with.

We can empower a new generation Web Writers with semantic HTML,
by creating Web Components for them.


🥧 What Web Developers will learn in this post

  • Create a static Pie Chart with SVG (a core browser technology)

  • Create a (very basic, yet powerful) <pie-chart> Web Component to write Pie Charts with semantic HTML

  • NO Frameworks, NO Libraries required!

<pie-chart>
  <slice size="90" stroke="green">HTML</slice>
  <slice size="1"  stroke="red">JavaScript</slice>
  <slice size="9"  stroke="blue">CSS</slice>
</pie-chart>
Enter fullscreen mode Exit fullscreen mode
  • I changed value to size because value is a programmers/maths term. size better expresses what the slice does

  • color became stroke because that is the stroke-color attribute name for SVG Elements (see below) and I don't want to confuse users with 2 different names for the same attribute

  • For demonstration purposes I have kept <pie-chart> Web Component functionality as minimal as possible

  • The use of the unknown element <slice> instead of <pie-slice> is shortly discussed at the bottom of this post. It warrants its own post, discussing pros and cons.


✔️ Web Component technologies used:

❌ Web Component technologies NOT used:

The last section of this post describes how these technologies can enhance a <pie-chart> v2.0 Web Component.


🥧 Step #1 - Designing the pie

A Pie Slice can easily be created with the SVG circle element:

  <circle stroke="green" stroke-dasharray="10 90" 
          pathLength="100" 
          cx="50%" cy="50%" r="25%" fill="none" stroke-width="50%">
  </circle>
Enter fullscreen mode Exit fullscreen mode
  • Key is the pathLength="100" attribute, declaring all calculations on the SVG element consider the <circle> 100 units in length.

  • Then stroke-dasharray="10 90" says:

    • draw a green stroke for 10 units
    • add whitespace for 90 units

Multiple slices are drawn with an extra stroke-dashoffset for each slice. The stroke-dashoffset value is the subtracted total of all previously drawn slices.

Each stroke-dashoffset is increased by 25 units, to make the Pie Chart start drawing at the top.

All SVG required for the static Pie Chart is:

<svg viewBox="0,0,200,200">
  <circle stroke="green" stroke-dasharray="10 90" stroke-dashoffset="25" 
          pathLength="100" 
          cx="50%" cy="50%" r="25%" fill="none" stroke-width="50%">
  </circle>
  <circle stroke="blue" stroke-dasharray="25 75" stroke-dashoffset="15" 
          pathLength="100" 
          cx="50%" cy="50%" r="25%" fill="none" stroke-width="50%">
  </circle>
  <circle stroke="red" stroke-dasharray="65 35" stroke-dashoffset="-10" 
          pathLength="100" 
          cx="50%" cy="50%" r="25%" fill="none" stroke-width="50%">
  </circle>
</svg>
Enter fullscreen mode Exit fullscreen mode

🥧 Step #2 - Creating the <pie-chart> Web Component


<pie-chart> SVG Helper methods

Make working with SVG easier (can be copied to any SVG project):

included in the JSFiddle source code as Base class SVGMeisterElement extends HTMLElement

  • createSVGElement ( { tag , [attributes] , [innerHTML] , [append] } )
    creates any SVG Element in the SVG NameSpace,
    optional parameters set all attributes, innerHTML and append child Elements
    The element is returned, not added to the DOM

  • createSVGCircle ( { configuration })
    creates a SVG <circle> from all configuration parameters


The custom HTML <pie-chart> is replaced with SVG, using the Web Components Custom Elements API

<pie-chart>
  <slice size="90" stroke="green">HTML</slice>
  <slice size="1"  stroke="red">JavaScript</slice>
  <slice size="9"  stroke="blue">CSS</slice>
</pie-chart>
Enter fullscreen mode Exit fullscreen mode
  • Each slice provides a size and stroke and a label
  • Each slice becomes a SVG <circle>

Web Component notes

  • The <pie-chart> Web Component is created once,

  • an HTML writer is never confronted with JavaScript code.

  • Contrary to traditional libraries, Custom Elements can also be defined AFTER usage in the DOM.
    existing Elements will automagically upgrade once the Custom Elements API defines the <pie-chart> Web Component.

  • If the <pie-chart> is not defined (yet) (or JavaScript is disabled)
    CSS creates a decent fallback:

    slice {
      display: block
    }
    slice::before {
      content: attr(size) "% "
    }
Enter fullscreen mode Exit fullscreen mode

output:

  90% HTML
  1% JavaScript
  9% CSS
Enter fullscreen mode Exit fullscreen mode

I have decided not to break up this post in two.

Posting the second part next week has no benefit.

If you a are bit overwhelmed by the first part; go grab a a cup of coffee
(or continue next week)


🥧 The Custom Element API bones of the <pie-chart> Web Component

customElements.define( "pie-chart" ,
  class extends SVGMeisterElement { // see JSFiddle, contains the SVG code
    connectedCallback() { // fires on the OPENING <pie-chart> tag
      // wait till <slice> elements are available in the DOM:
      setTimeout(() => this.renderPieChart()); 
    }
    renderPieChart() {
      // ... some configuration variables here, see source code
      this.svg = this.createSVGElement({ // create <svg> Element
        tag: "svg",
        attributes: {
          viewBox: `0 0 ${this.width} ${this.height}`,
        },
        innerHTML: `...`, // default SVG innerHTML content
        append: this.createSlicesWithCircles() // append <circle>s
      });
      this.replaceWith(this.svg); // replace <pie-chart> with <svg>
      this.slices.forEach((slice) => { // loop all <cicle> elements
        const sliceMiddlePoint = slice.getPointAt(this.labelPosition);
        // ... append label
      });
    }
    createSlicesWithCircles() { // process all <slice> inside <pie-chart>
      let offset = 25;
      const slices = [...this.querySelectorAll("slice")];
      // all <slice> elements are returned as <circle>
      this.slices = slices.map((slice) => { 
        // read size from <slice size="90">
        const size = parseFloat(slice.getAttribute("size")); 
        let circle = this.createSVGCircle({ // SVG helper method
          size,
          offset,
          stroke: slice.getAttribute("stroke") // read stroke color
        });
        offset -= size; // every slice at next offset
        return circle;
      });
      return this.slices;
    }
  });
Enter fullscreen mode Exit fullscreen mode

Code notes:

  • The standard connectedCallback method is called the moment the opening <pie-chart> tag is appended to the DOM

  • thus setTimeout (or anything that waits till the Event Loop is done) is required to wait till all <slice> elements are parsed by the Browser engine. (see StackOverflow post: https://stackoverflow.com/a/70952159/2520800)

  • the renderPieChart method

    • creates an <svg>
    • reads all <slice> and adds them as <circle>
  • again: It does not matter when the Web Component is defined.
    Above code can be executed before or after page load.

Full working code:

  • No Frameworks! No Libraries! No External code!


🥧 Enhancements with more Web Component techologies

Disclaimer: Code Snippets are not full working code, presented to inspire you only.

shadowDOM

Replacing HTML is a bit crude and not flexible. With shadowDOM the SVG can be displayed, and the <pie-chart> HTML will remain active but invisible in the DOM (then called lightDOM)

The Custom Elements API code can be extended with:

constructor() {
  // Documentation that says "use super first in the constructor" is wrong
  let svg = `<svg>...</svg>`; 
  super() // sets and returns this scope
    .attachShadow({mode:"open"}) // sets and returns this.shadowRoot
    .innerHTML = svg;
  this.svg = this.shadowRoot.querySelector("svg");
}
Enter fullscreen mode Exit fullscreen mode

then the line in the renderPieChart method can be deleted

this.replaceWith(this.svg); // replace <pie-chart> with <svg>
Enter fullscreen mode Exit fullscreen mode

slots

SLOTs are placeholders for more complex user-defined content, while still keeping the Web Component in control of how and where the slot content is displayed. With title and description slots defined in the Web Component a <pie-chart> 2.0 could look like:

<pie-chart>
  <div slot="explanation">
    ... any HTML content here
  </div>
  <h1 slot="title">Web Technologies</h1>
  <slice size="90" stroke="green">HTML</slice>
  <slice size="1"  stroke="red">JavaScript</slice>
  <slice size="9"  stroke="blue">CSS</slice>
</pie-chart>
Enter fullscreen mode Exit fullscreen mode

See <template> below where the slot content is used

Related:

templates

Templates are re-usable inert snippets of HTML. Can be created in HTML or by Script. Allowing very flexible creation, styling and configuration of (multiple) Web Components:

<template id="PIE-CHART">
  <style>
    /* CSS */
  </style>
  <slot name="title">A Pie Chart<!-- replaced with userdefined content --></slot>
  <svg>
    <defs>
      <filter x="0" y="0" width="1" height="1" id="label">
        <feFlood flood-color="#222" flood-opacity="0.4"/>
         <feMerge><feMergeNode/><feMergeNode in="SourceGraphic"/></feMerge>
      </filter>
    </defs>
  </svg>
  <slot name="description"><!-- userdefined content goes here--></slot>
</template>
Enter fullscreen mode Exit fullscreen mode

A constructor can read Templates (in this example an existing DOM <template>)

constructor() {
  // Documentation that says "use super first in the constructor" is wrong
  let template = (id) => this.getElementById(id).content.cloneNode(true);
  super() // sets and returns this scope
    .attachShadow({mode:"open"}) // sets and returns this.shadowRoot
    .append( template( this.nodeName ) );
  this.svg = this.shadowRoot.querySelector("svg");
}
Enter fullscreen mode Exit fullscreen mode

observedAttributes

Normal HTML behaviour allow attribute changes to affect what the HTML does/displays.

In the Custom Elements API you can specify which attributes enforce this behaviour

<pie-chart offset="10">
  <slice size="90" stroke="green">HTML</slice>
  <slice size="1"  stroke="red">JavaScript</slice>
  <slice size="9"  stroke="blue">CSS</slice>
</pie-chart>
Enter fullscreen mode Exit fullscreen mode
static get observedAttributes(){
  return ["offset"]
}

attributeChangedCallback( name, oldValue, newValue ){
  if( name=="offset"){
    this.renderPieChart()
  }
}
Enter fullscreen mode Exit fullscreen mode

Now on every offset change the Pie Chart will be rendered with new settings

::part CSS Selector - shadowParts

Since shadowDOM is protected from global CSS manipulation.
Specified parts of the Web Component shadowDOM can be exposed to the 'outside world' for global CSS configuration.

Font-styles and CSS properties do cascade into shadowDOM; see:

<template id="PIE-CHART">
  <slot part="title" name="title">
     A Pie Chart<!-- replaced with userdefined content -->
  </slot>
</template>
Enter fullscreen mode Exit fullscreen mode

global CSS will now style all titles in all <pie-chart> elements

::part(title){
  text-transform: capitalize;
  background: beige;
  border-bottom: 2px solid green;
}
Enter fullscreen mode Exit fullscreen mode

lifecycle callbacks - also see this diagram

  • constructor
    Called once

  • connectedCallback
    Called on the opening tag of the Web Component, and every time the Element is moved in the DOM (think drag-drop like situations)

  • attributeChangedCallback
    Called by every update of an observed attribute

  • adoptedCallback
    When moving Elements between multiple documents

  • disconnectedCallback
    Called when the Element is removed from the DOM


🥧 To <slice> or not to <pie-slice>, that is the question

  • <slice> is not a valid HTML Element, a Linter will complain, but its a valid XML/DOM Element

  • The <pie-chart> Web Component works fine with <slice>.

  • Nor is it a (defined) Custom Element, which always require at minimum one hyphen (-) in the tagName to distinguish it from (future) HTML Elements.

  • So <pie-slice> is also an option, but doesn't have to be a defined Custom Element

  • For more pro and cons, see: Unknown Elements for better semantic HTML

🥧 Some afterthoughts

  • Trying to do a complete Web Components course in one Dev post is impossible

  • SVG Elements (like <circle> can not (yet) be extended

  • The Custom Elements API only allows extending HTMLElement. Proper name: Autonomous Elements

  • Extending (Customized Built-In) HTML Elements like <button> is not supported in Safari (and won't be)

  • An Element <pie-slice> would allow for observedAttributes to work; something that can otherwise only be accomplished by applying the MutationObserver API.

  • I didn't go into ES Class OOP functionality. See: https://javascript.info/class

  • Copy the JSFiddle, play and learn
    https://jsfiddle.net/WebComponents/3kwn4f7e/




Top comments (2)

Collapse
 
westbrook profile image
Westbrook Johnson

Really like the use of HTML elements as "data model" here, this is something I spend a lot of time trying to get working well. For features like this, it is such a no brainer and I'm happy to see it in use over the data="[]" example you shared. I've been playing with the Reactive Controller paradigm proposed by the Lit team in their pending announcement to position this as something a little more reusable: webcomponents.dev/edit/F4jBbQpeMSu... would love to hear your thoughts!

Side note: I was struck by how much time I spent confirming for myself that <slice> is not a native HTML element. I'd say, beyond the possibility of leveraging observedAttributes, that it's possible a custom element would reduce confusion around what's natively available vs added via your code. Was certainly cool to see the simplicity, but wonder how others with less custom element experience actually consume that data.

Collapse
 
dannyengelman profile image
Danny Engelman • Edited

It depends on what the team needs. If you do not have the JS team, than HTML opens up a lot of possibilities.
You can pick anyone off the street, hand them the Web Component documentation and they will be productive within a day.

THAT is what made the Web huge and great in the 90s. There were plenty of other technologies available. Before I became a Web-Developer, I was a Gopher-Developer for years.

"other technologies" is why I am reluctant to use Lit. There are still too many alternatives.
I had to ditch 500K (euros.. not dollars) of MooTools development in 2009, because my CTO predecessor had allowed two 24 year old contractors to select the technology for the CEO his pet-project.
When I asked "What about jQuery?" they answered: "What is that?"
(And when we CTO and CFO in Good Cop Bad Style put the CEO on the spot...
the CTO was fired; and the CFO quit her job the next day)

It learned me the game is not played on the board...

Lit is Google and the WHATWG is Google, Mozilla, Microsoft and Apple.
And both Ryosuke Niwa (Apple) and Anne van Kesteren (Mozilla) have shown in their responses, it is no longer the V0 party where Google throws something against the wall, hoping it sticks.
Ofcourse progress is slow with 4 parties having to agree; but its slow and sure
I have never seen co-operation like this between Internet companies in the past 31 years.

Now, I am in the position to spend that extra 10%? 15%? 20%? time to do everything with Vanilla.
I can ask clients: "Do you want software that will run for sure for the next 25 JavaScript years?"
And have the financial freedom to decline clients that don't.
Because I learned one thing in all those years:
Software needs to be maintained, and developers spent 50% trying to understand previous developers code.

That's why I plea for HTML skills, the power of the Web to everyone who can type and save a document.

But is it really 10% to 20% extra?
Guess how much time it took me to restore the Drupal blog I used 17 years ago;
because stupid me preferred a better technical solution over WordPress

I have my "Game Board" pet-project/Web Component test-project hexedland.com
that is going to run for the next 25 years without problems.
Because I am building with ZERO dependencies... well only one.. The Web

And since the W3C handed the keys to the WHATWG in 2019; that future is in the hands of 4 companies... and to date, they haven't invited Facebook.
So I am no longer wasting energy on React; not even answering React questions on StackOverflow. (I try to answer every Web Component question, because it makes me learn)

And that Drupal site with all my old blogs? I stopped trying after 2 hours.

My next post will detail on the pros and cons of UNknown Elements.
Or the risk of using (new) WHATWG defined Elements ( <progress> is an existing HTML tag)

The Base Class is the <pie-chart> from the first post:

<progress-circle>
  <progress value="75%" stroke="green">SEO</progress>
  <progress value="60%" stroke="orange">Social</progress>
  <progress value="65%" stroke="teal" edge="black">Maps</progress>
  <progress value="50%" stroke="orangered">Traffic</progress>
</progress-circle>
Enter fullscreen mode Exit fullscreen mode