Create SVG charts and animations without a JS Library

June 3, 2018

At Dreamsolution we need to visualize a lot of data using bar charts, pie charts, counters etc. Most of the time we use the Highcharts library for the charts. Now I had some use cases where a library as Highcharts is overdone. The question was how can I create a simple pie and bar chart without using a library (Highcharts, Greensock, Raphael, SnapSVG and SVG.js)?

Use case: pie chart, bar chart

The designer already created a mockup how the bar and pie chart should look like, only thing I need to do is to make it work on the web. The values for the charts come from the database, so using CSS animations in a separate file would not do the trick. (Unless you can serve your solution to the latest greatest browers, using CSS custom properties).

You can also use inline CSS to be able to work with dynamic values, but if you have a CSP in place, then inline CSS are blocked due to security reasons.

It was time to start playing with SMIL.

Pie chart

I create a pie chart and animate the stroke-dasharray using the animate tag. Looks pretty clean and readable.

    
    <svg viewbox="0 0 400 400">
        <!-- main circle -->
        <circle fill="#ccc" cx="200" cy="200" r="200" />
        <!-- circle where the dash-stroke will be animated -->
        <circle cx="200" cy="200" r="160" transform="rotate(-90, 200, 200)"
                stroke-dasharray="0, 1000" stroke="#7cb342" data-fallback="edge">
            <animate attributeName="stroke-dasharray" dur="1s"
                    to="300,1000" fill="freeze" />
        </circle>
        <!-- Inner circle -->
        <circle cx="200" cy="200" r="160" fill="#fff" />
    </svg>
    
    

Demo pie chart:

30%

If you also want the percentage (counter) be animated the code looks like this:

    
    <svg viewbox="0 0 400 400">
        <defs>
            <!-- Clip path creates a clipping region that defines what part of
            an element should be displayed in svg.
            Only the correct portion of the text is shown, the rest is hidden.-->
            <clipPath id="counter-clippath">
                <rect x="50" y="0" width="320" height="72" />
            </clipPath>
        </defs>
        <!-- main circle -->
        <circle fill="#ccc" cx="200" cy="200" r="200" />
        <!-- circle where the dash-stroke will be animated -->
        <circle cx="200" cy="200" r="160" transform="rotate(-90, 200, 200)"
                stroke-dasharray="0, 1000" stroke="#7cb342" stroke-width="80" data-fallback="edge">
            <animate attributeName="stroke-dasharray" dur="1s"
                    to="300,1000" fill="freeze" />
        </circle>
        <!-- Inner circle -->
        <circle cx="200" cy="200" r="160" fill="#fff" />
        <g class="counter-clippath" clip-path="url(#counter-clippath)"
            transform="translate(0, 165)">
            <g class="move-svg-text">
                <animateTransform attributeName="transform" type="translate"
                    dur="1s" calcMode="discrete" values="0 0; 0 -90; 0 -180; 0 -270; 0 -360; 0 -450; 0 -540" fill="freeze" />
                <text x="200" y="70" text-anchor="middle" font-size="100" fill="#3c4946">1%</text>
                <text x="200" y="160" text-anchor="middle" font-size="100" fill="#3c4946">3%</text>
                <text x="200" y="250" text-anchor="middle" font-size="100" fill="#3c4946">5%</text>
                <text x="200" y="340" text-anchor="middle" font-size="100" fill="#3c4946">7%</text>
                <text x="200" y="430" text-anchor="middle" font-size="100" fill="#3c4946">9%</text>
                <text x="200" y="520" text-anchor="middle" font-size="100" fill="#3c4946">11%</text>
                <text x="200" y="610" text-anchor="middle" font-size="100" fill="#3c4946">13%</text>
            </g>
        </g>
    </svg>
    
    
1% 3% 5% 7% 9% 11% 13%

Demo bar chart

The code for a simple bar chart:

    
    <svg viewBox="0 0 190 165">
        <g transform="translate(20, 0)">
            <line class="barchart_line bar-1"
                    x1="0" y1="150"
                    x2="0" y2="0"
                    stroke-width="19" stroke-dasharray="0, 150">
                <animate attributeName="stroke-dasharray" dur="2s"
                        from="0, 150"
                        to="150, 150"
                        begin="startanimation+200ms"
                        fill="freeze" restart="whenNotActive" />
            </line>
            <text x="0" y="158" class="bar-legend" text-anchor="middle">14 may</text>
        </g>
        <g transform="translate(45, 0)">
            <line class="barchart_line bar-2"
                    x1="0" y1="150"
                    x2="0" y2="0"
                    stroke-width="19" stroke-dasharray="0, 150">
                <animate attributeName="stroke-dasharray" dur="2s"
                        from="0, 150"
                        to="50, 150"
                        begin="startanimation+400ms"
                        fill="freeze" restart="whenNotActive" />
            </line>
            <text x="0" y="158" class="bar-legend" text-anchor="middle">15 may</text>
        </g>
        <g transform="translate(70, 0)">
            <line class="barchart_line bar-3"
                    x1="0" y1="150"
                    x2="0" y2="0"
                    stroke-width="19" stroke-dasharray="0, 150">
                <animate attributeName="stroke-dasharray" dur="2s"
                        from="0, 150"
                        to="80, 150"
                        begin="startanimation+600ms"
                        fill="freeze" restart="whenNotActive" />
            </line>
            <text x="0" y="158" class="bar-legend" text-anchor="middle">16 may</text>
        </g>
        <g transform="translate(95, 0)">
            <line class="barchart_line bar-4"
                    x1="0" y1="150"
                    x2="0" y2="0"
                    stroke-width="19" stroke-dasharray="0, 150">
                <animate attributeName="stroke-dasharray" dur="2s"
                        from="0, 150"
                        to="110, 150"
                        begin="startanimation+800ms"
                        fill="freeze" restart="whenNotActive" />
            </line>
            <text x="0" y="158" class="bar-legend" text-anchor="middle">17 may</text>
        </g>
        <g transform="translate(120, 0)">
            <line class="barchart_line bar-5"
                    x1="0" y1="150"
                    x2="0" y2="0"
                    stroke-width="19" stroke-dasharray="0, 150">
                <animate attributeName="stroke-dasharray" dur="2s"
                        from="0, 150"
                        to="80, 150"
                        begin="startanimation+1000ms"
                        fill="freeze" restart="whenNotActive" />
            </line>
            <text x="0" y="158" class="bar-legend" text-anchor="middle">18 may</text>
        </g>
        <g transform="translate(145, 0)">
            <line class="barchart_line bar-6"
                    x1="0" y1="150"
                    x2="0" y2="0"
                    stroke-width="19" stroke-dasharray="0, 150">
                <animate attributeName="stroke-dasharray" dur="2s"
                        from="0, 150"
                        to="140, 150"
                        begin="startanimation+1200ms"
                        fill="freeze" restart="whenNotActive" />
            </line>
            <text x="0" y="158" class="bar-legend" text-anchor="middle">19 may</text>
        </g>
        <g transform="translate(170, 0)">
            <line class="barchart_line bar-7"
                    x1="0" y1="150"
                    x2="0" y2="0"
                    stroke-width="19" stroke-dasharray="0, 150">
                <animate attributeName="stroke-dasharray" dur="2s"
                        from="0, 150"
                        to="70, 150"
                        begin="startanimation+1400ms" fill="freeze" restart="whenNotActive" />
            </line>
            <text x="0" y="158" class="bar-legend" text-anchor="middle">20 may</text>
        </g>
    </svg>
    
    
14 may 15 may 16 may 17 may 18 may 19 may 20 may

Issues:

As you can see it is easy to create simple pie chart and bar chart svg with animation without using a library. However I did bump into some issues in Edge and Firefox.

It turns out that Microsoft Edge does not support SMIL, while other browsers do.

If the animation is not a deal breaker you can set the end state of the stroke-dasharray for Edge by using Javascript. You check if SMIL is not supported and do your thing for IE, Edge:

    
    var has_smil = Boolean(document.createElementNS) & &
            (/SVGAnimate/).test(document.createElementNS('https://www.w3.org/2000/svg', 'animate'));
    // fallback for IE + Edge which don't support SMIL animation. Set directly the elm end state.
    if (!has_smil) {
        var elm = document.querySelector('[data-fallback="edge"]');
        var a = elm.querySelector('animate');
        var end_state = a.getAttribute('to');
        elm.setAttribute(a.getAttribute('attributeName'), end_state);
    }
    
    

Firefox and CSP

When you are using CSP, you can get inline-css CSP error in Firefox when animating the following SVG attributes:

For some vague reason Firefox's CSP engine thinks that these svg attributes used in the animation are inline styles. If you animate the width, or height of an svg element then Firefox has no issues. I reported this bug at Bugzilla. Depending on your project you can use for Firefox the iframe workaround: load the svg in an iframe with a less strict CSP.

Conclusion

Can you use SMIL for production sites? If you don't need to fully support Edge and can use the iframe workaround for Firefox when working with a CSP, or if you use chromebit for narrowcasting, then yes you can. For cross browser support you still need a library.

I hope that in the near future all the major browsers will support SMIL. It is easy to use and SMIL3.0 is available since 2008. For more examples go to CSS Tricks.