Further adventures in SVG, Gauges and Trigonometry

Gauges, the types of which I discussed in my last post, are pretty useless without target zones showing people where the needle should be pointing. With that in mind, it's probably a good idea that we add them to our gauges before they become part of the dashboard. This, of course, means drawing SVG arcs. Which means I need to know where a particular point is on the circumference of a circle.

Which means trigonometry.

I really enjoyed maths at school - two really great teachers helped - even going so far as to enjoy trigonometry. Sadly, as I only use a small portion of the maths I used to be quite fluent in at school on a day-to-day basis, most of the things I used to know now escape me, without a quick trip down Google's memory lane.

Even a fairly basic task - finding the x and y coordinates of a point at a given angle, on the circumference of a circle of radius r, centred at the coordinates cx,cy - saw me grind to a halt for a minute or two this morning. Then you find what you were looking for, and you remember it was dead easy all along:

Trigdiagram

Adding the zones to a circular gauge means I need to know where the arc starts, and where it finishes. Fortunately, SVG will worry about the rest for me, but I need the x,y co-ordinate pairs for four points on the zone.

Gauge-zone

Sure, we could calculate these by hand and hard-code them in, but given that the gauge is going to change and be re-used, it's better - and easier - to get some code to do the calculations for us. Good job that SVG supports JavaScript, then:

    function c_xy(r, a, cx, cy) {
        var cx = 110 + (r * Math.cos(a * (Math.PI/180)));
        var cy = 110 - (r * Math.sin(a * (Math.PI/180)));
        return { 
            x: cx,
            y: cy
        };
    }

This function will return an object with the x and y coordinate of a point at angle a on the circumference of a circle of radius r. All it's doing is exactly what the equations on the chart above show.

So now we have a way of finding the co-ordinates of the points on the zone, let's create a function that will add a zone to the gauge:

    function gauge_addZone(pct1, pct2, fill, className, r1, r2) {
        var a1 = (-270*(pct1/100))+225;
        var a2 = (-270*(pct2/100))+225;
        var c1s = c_xy(r1, a1 % 360, 110, 110);
        var c1f = c_xy(r1, a2 % 360, 110, 110);
        var c2s = c_xy(r2, a2 % 360, 110, 110);
        var c2f = c_xy(r2, a1 % 360, 110, 110);
        
        var zone = document.createElementNS("http://www.w3.org/2000/svg", "path");
        var d = " M " + c1s.x + "," + c1s.y +
                " A " + r1 + "," + r1 + " " + (pct2-pct1 > (200/3) ? "0 1" : "1 0") + " 1 " + c1f.x + "," + c1f.y + 
                " L " + c2s.x + "," + c2s.y + 
                " A " + r2 + "," + r2 + " " + (pct2-pct1 > (200/3) ? "0 1" : "1 0") + " 0 " + c2f.x + "," + c2f.y + 
                " Z";
        zone.setAttribute("d", d);
        zone.setAttribute("fill", fill);
        if (className != null) {
            zone.setAttribute("class", className + " gaugeZone");
        } else {
            zone.setAttribute("class", "gaugeZone")
        }
    
        document.getElementById("gaugeBack").appendChild(zone);
    }

This function does all the heavy lifting. The gauges I'm creating all work on the theory of ranges from 0% to 100%, so by passing in the starting and finishing percentages - pct1 and pct2 - of the target zone, I've a good idea where I'm expecting the zone to be drawn.

The fill value allows me to pass in a colour, if I so desire, and I'm hoping className should be fairly self-explanatory too. The r1 and r2 values allow me to set the thickness of the zones. For the zone in the example above, I've used 80 and 55, but if I want a thin zone to be drawn, I could specify radii of 80 and 75, resulting in something like this:

Screen_shot_2010-06-15_at_12

Now it's time to use our c_xy function to get our co-ordinate objects for the four points. The zone is created as a path element, and we draw using the d attribute. I won't go into the actual syntax, but it's safe to say that it's fairly simple to pick up. SVG Basics has some good tutorials on paths and arcs that you can read if you're so inclined.

Finally, because we don't really want to hard-code the position of the pointer, let's create a function that will move the pointer for us. This one's really basic, but you could extend it to make a smoothly animated pointer with minimal fuss:

    function gauge_setPointer(pct) {
        var a = (270 * (pct/100));
        document.getElementById("gaugeNeedle").setAttribute("transform", "rotate(" + a + ",110,110)");
    }

All we're doing, because we know that the angular range from 0% to 100% spans 270°, is multiplying that angle by the percentage, and then applying a transformation to the gauge needle. Very simple.

Finally, we actually call the functions:

    gauge_addZone(80, 100, "#0f0", "zoneGood", 80, 55);
    gauge_setPointer(83.5);

Which gives us exactly what you've seen above:

Gauge-zone

The code I've used above means that we can also add multiple zones, should we wish. If, as well as showing a safe zone, you want to also add a zone which should be avoided, you can do that. All we'd need to do would be to add another gauge_addZone line, and we're done:

    gauge_addZone(80, 100, "#0f0", "zoneGood", 80, 55);
    gauge_addZone(0, 40, "#f00", "zoneBad", 80, 55);
    gauge_setPointer(36.8);

This would result in the following example:

Screen_shot_2010-06-14_at_18

I'm really enjoying using SVG, and I'll probably post more on it as time goes by.

SVG and today's web browsers

Our intranet's homepage, like many others, features a dashboard - a bunch of key metrics designed to give users an idea of how they're doing, at a glance. In it's current guise, it's fairly limited, featuring the three latest news items, and three metrics relating to stock listings, photography and pricing.

Screen_shot_2010-06-11_at_12

In the 18 months since we re-launched our intranet, we've created loads of new applications, added tons of new features, and hooked into many new parts of the business that were previously unavailable to us. Many different brands use different systems, so we're keen to make sure that anyone, from any part of the business, is able to understand what's going on.

We're redesigning our dashboard, to feature a lot more instant at-a-glance insights into what's going on, and to show our staff, managers and directors what needs their focus. We've had lots of feedback on the gauges, with the majority liking the instant colours but finding the graphs themselves lacking something.

So given that SVG seems like the best candidate for the job, I set about creating a rough idea of what my gauge would look like using very basic shapes, colours and gradients in Adobe Illustrator:

Screen_shot_2010-06-11_at_13

Illustrator's SVG export was a little mark-up heavy, so I scrapped it, fired up TextMate and put this SVG together:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve">
    <defs>
        <!-- Gradients for the gauge bevel -->
        <linearGradient id="grShOuter" x1="0%" x2="100%" y1="100%" y2="0%">
            <stop offset="0" style="stop-color: #e9e9e9" />
            <stop offset="1" style="stop-color: #898989" />
        </linearGradient>
        <linearGradient id="grShInner" x1="0%" x2="100%" y1="100%" y2="0%">
            <stop offset="0"   style="stop-color: #f9f9f9" />
            <stop offset="0.4" style="stop-color: #d9d9d9" />
            <stop offset="0.5" style="stop-color: #898989" />
            <stop offset="0.6" style="stop-color: #d9d9d9" />
            <stop offset="1"   style="stop-color: #f9f9f9" />    
        </linearGradient>
    
        <!-- Gradient for the gauge background -->
        <radialGradient id="grGaugeBack" cx="50%" cy="100%" r="85%">
            <stop offset="0" style="stop-color: #666" />
            <stop offset="1" style="stop-color: #000" />
        </radialGradient>

        <!-- Gradient for the needle itself -->
        <linearGradient id="grNeedle" x1="0%" x2="100%" y1="0%" y2="0%">
            <stop offset="0"       style="stop-color: #f9f9f9" />
            <stop offset="0.4995" style="stop-color: #efefef" />
            <stop offset="0.5005" style="stop-color: #d9d9d9" />
            <stop offset="1"       style="stop-color: #cfcfcf" />
        </linearGradient>
    
        <!-- Gradients for the needle cap -->
        <linearGradient id="grCapOuter" x1="0%" x2="0%" y1="0%" y2="100%">
            <stop offset="0" style="stop-color: #292929" />
            <stop offset="1" style="stop-color: #898989" />
        </linearGradient>
        <linearGradient id="grCapInner" x1="0%" x2="0%" y1="100%" y2="0%">
            <stop offset="0" style="stop-color: #393939" />
            <stop offset="1" style="stop-color: #797979" />
        </linearGradient>
    
        <!-- Tick marks -->
        <rect id="tickMajor" x="30" y="108"  width="25"   height="4"   transform="rotate(-45,110,110)" />
        <rect id="tickMinor" x="30" y="109.25" width="12.5" height="1.5" transform="rotate(-45,110,110)" />
        <g id="tickMinorGroup">
            <use xlink:href="#tickMinor" transform="rotate( 13.5,110,110)" />
            <use xlink:href="#tickMinor" transform="rotate( 27  ,110,110)" />
            <use xlink:href="#tickMinor" transform="rotate( 40.5,110,110)" />
        </g>
        <g id="gaugeTicks">
            <use xlink:href="#tickMajor" />
            <use xlink:href="#tickMinorGroup" />
            <use xlink:href="#tickMajor"        transform="rotate( 54,110,110)" />
            <use xlink:href="#tickMinorGroup"    transform="rotate( 54,110,110)" />
            <use xlink:href="#tickMajor"         transform="rotate(108,110,110)" />
            <use xlink:href="#tickMinorGroup"    transform="rotate(108,110,110)" />
            <use xlink:href="#tickMajor"         transform="rotate(162,110,110)" />
            <use xlink:href="#tickMinorGroup"    transform="rotate(162,110,110)" />
            <use xlink:href="#tickMajor"         transform="rotate(216,110,110)" />
            <use xlink:href="#tickMinorGroup"    transform="rotate(216,110,110)" />
            <use xlink:href="#tickMajor"         transform="rotate(270,110,110)" />
        </g>
    </defs>

    <!-- The Gauge -->
    <g id="gaugeBack">
        <circle fill="url(#grShOuter)" cx="110" cy="110" r="100" />
        <circle fill="url(#grShInner)" cx="110" cy="110" r="95" />
        <circle cx="110" cy="110" r="88" />
        <circle fill="url(#grGaugeBack)" cx="110" cy="110" r="85" />
    </g>

    <!-- Tick marks -->
    <use xlink:href="#gaugeTicks" fill="#fff" opacity="0.35" />

    <g id="gaugePointer">
        <polygon id="gaugeNeedle" fill="url(#grNeedle)" points="107.5,42.5 110,40 112.5,42.5 117.5,135 110,137.5, 102.5,135" transform="rotate(-135,110,110)" />
        <circle fill="url(#grCapOuter)" cx="110" cy="110" r="15" />
        <circle fill="url(#grCapInner)" cx="110" cy="110" r="12" />
    </g>
</svg>

Those of you in the know won't be surprised by IE6, IE7 and IE8's efforts, but I was a little underwhelmed by the IE9 platform preview. There doesn't appear to be any gradient support, and when you do use them use the opacity attribute, you get very odd artefacts on the resulting image:

Svg-comparison

I was, however, very pleased with the other browser's results - there's no need to show you the individual images generated by Firefox, Safari/Chrome and Opera, as they really did look practically identical - and given that we're going to be settling on SVG for the gauges, I needed to find a solution to the IE problem.

Enter SVGWeb

SVGWeb is something I first heard about last September, but wasn't quite useful enough for us to go ahead with. Now, although it's by no means even a beta release, I've found it to be stable enough in testing to do what we need it to do - simply, render the gauge, in Internet Explorer as close as possible to the output we get on Mozilla and WebKit browsers.

All it requires is the addition of a script to your <head>...

<!--[if lte IE 8]><script src="svgweb/svg.js" data-path="svgweb"></script><![endif]-->

...and a slight change to the way in which you add your SVG markup or files to your page - instead of simply adding an <svg> tag, you add your SVG using an <object> tag - not ideal, but certainly not a show-stopper:

Et voila:

Ie6-ie8

There are some very slight differences between the native output from the higher quality browsers, mostly with regards to the opacity of the tick marks, but it's close enough that we can commit to using SVG now and, thanks to the Internet Explorer team's commitment to providing SVG compatibility in IE9, in the future.

Why not Flash or Silverlight?

This article wasn't intended to be an attack on Flash, and I don't want to start any religious wars, but I do really believe that Flash has had it's time. More and more things that we used to require things like Flash for are now easier to create and more accessible when made with open web standards such as SVG, HTML and CSS. For what we require in this particular example, using Flash or Silverlight would be overkill - in fact, I'd rather resort to using server-generated images than using plugins to create the gauges.

The obvious and highly publicised benefit of not using a plugin, of course, is that the content works on mobile devices without the Flash plugin, and given that our company is pretty much standardising on HTC Android devices, with the occasional iPhone and iPad, it means that our lovely new graphics will, to borrow a phrase, 'just work'.

More and more companies are turning away from Flash for infographics and charts - Campaign Monitor have just announced that they've switched from Flash over to the excellent HighCharts JavaScript graphing library. Given that there are excellent graphing libraries out there, I can't imagine that major applications like Google Analytics will continue using Flash for much longer either.