Spinning SVG Animation

Interested in learning a few capabilities of SVG animation this evening from JavaScript in a web page, I put together a simple demonstration.

Click on the image to launch the demo.

image

Animating SVG in a modern browser (including IE9) is generally easy enough. This example was slightly more interesting in that I wanted the thicker line to rotate based on the center point of the image, rather than the location of the line itself.

Not what I wanted:

image

Desired rotation:

image

The white bar would rotate around the center point (marked by the red arrow above).

I’ve included all of the code at the bottom of the post. (There’s quite a lot of path information).

The JavaScript just used an interval to move the line around the point:

(function () {
    window.onload = loaded;
    function loaded() {
        var colorTemp = document.getElementById("color-temp");
        var reading = document.getElementById('current-reading');
        var currentAngle = 0;
        var fill;

        var direction = 1;
        setInterval(function () {
            currentAngle += direction;
            if (currentAngle >= 120 || currentAngle <= -120) {
                direction *= -1;
            } else if (currentAngle === 0) {
                fill = direction === 1 ? "#BE1E2D" : "#10A2DC";
                colorTemp.setAttribute("fill", fill);
            }
            // adjust the opacity
            colorTemp.setAttribute("opacity", Math.abs(currentAngle) / 120.0);

            reading.setAttribute("transform", "rotate(" + currentAngle + ")");

        }, 25);
    }
})();

The angle is fixed between 120 and –120 degrees. When the angle reaches zero, the fill color is toggled.

As the angle adjusts, the line is updated as well as the opacity of the fill.

The overall size of the SVG drawing is 600×600. Knowing that (and wanting the exact center), I translated a group to an offset of 300x, 300y containing the line:

<g id="temp-transform" transform="translate(300,300)">
        <line id="current-reading" fill="none" stroke="#FFFFFF"
              stroke-width="5" stroke-linecap="round"
              stroke-miterlimit="10" x1="0" y1="-180" x2="0" y2="-120"/>
</g>

This effectively made the rotation now work from the center, once the line coordinates were adjusted to reflect the new translation.

This works because the contents of the outer group caused the new starting point (origin) to be 300x, 300y rather than the default 0x, 0y. You can see the line is from y –120 to y –180. That’s because I wanted the line to start in the top middle (as the x is set to 0 for the line).

I used Adobe Illustrator CS 5.5 to create the SVG image. Illustrator typically does not always handle fonts well when exporting as SVG. The final text node was this:

<text transform="matrix(1 0 0 1 254.1387 83.3198)" fill="#D1D3D4" font-family="'Arial'"
font-weight="bold" font-size="29.4616">thermo</text>

To make it work across browsers, I needed to modify the text node below slightly. Note the font name and missing bold attribute. It’s not a hard switch, but it’s annoying if you need to make changes to your SVG multiple times.

<text transform="matrix(1 0 0 1 254.1387 83.3198)" fill="#D1D3D4" 
font-family="'Arial-BoldMT'" font-size="29.4616">thermo</text>

If you have any questions, please leave a comment!

<!DOCTYPE HTML>
<html>
<head>
    <title>Svg Spinner Demo</title>
    <style>
        #demo1 svg {
            width: 300px;
            height: 300px;
        }

    </style>
</head>
<body>

<div id="demo1">
<svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
     width="600px" height="600px" viewBox="0 0 600 600" enable-background="new 0 0 600 600" xml:space="preserve">
<g>
    <radialGradient id="SVGID_1_" cx="298.9106" cy="196.146" r="399.267" gradientUnits="userSpaceOnUse">
        <stop  offset="0" style="stop-color:#D9D9D9"/>
        <stop  offset="1" style="stop-color:#C2C2C2"/>
    </radialGradient>
    <path fill="url(#SVGID_1_)" d="M300.646,595.41c-78.957,0-153.188-30.747-209.02-86.579C35.795,453,5.048,378.77,5.048,299.812
        c0-78.957,30.748-153.188,86.579-209.02S221.689,4.214,300.646,4.214c78.958,0,153.188,30.748,209.019,86.579
        c55.832,55.831,86.579,130.063,86.579,209.02c0,78.958-30.747,153.188-86.579,209.019
        C453.834,564.663,379.604,595.41,300.646,595.41z"/>
    <path fill="#808285" d="M300.646,6.214c39.637,0,78.086,7.762,114.281,23.071c34.961,14.787,66.359,35.957,93.324,62.922
        s48.135,58.364,62.923,93.325c15.309,36.193,23.07,74.643,23.07,114.28s-7.762,78.086-23.07,114.281
        c-14.788,34.961-35.958,66.359-62.923,93.324s-58.363,48.135-93.324,62.923c-36.194,15.309-74.644,23.07-114.281,23.07
        s-78.087-7.762-114.28-23.07c-34.961-14.788-66.36-35.958-93.325-62.923s-48.135-58.363-62.922-93.324
        C14.81,377.898,7.048,339.449,7.048,299.812s7.762-78.087,23.071-114.28c14.787-34.961,35.957-66.36,62.922-93.325
        s58.364-48.135,93.325-62.922C222.559,13.976,261.009,6.214,300.646,6.214 M300.646,2.214
        C136.287,2.214,3.048,135.453,3.048,299.812S136.287,597.41,300.646,597.41s297.598-133.239,297.598-297.598
        S465.005,2.214,300.646,2.214L300.646,2.214z"/>
</g>
<g>
    <radialGradient id="SVGID_2_" cx="299.0464" cy="204.2632" r="368.0053" gradientUnits="userSpaceOnUse">
        <stop  offset="0" style="stop-color:#141414"/>
        <stop  offset="1" style="stop-color:#080808"/>
    </radialGradient>
    <path fill="url(#SVGID_2_)" d="M300.646,572.266c-72.775,0-141.194-28.34-192.654-79.8c-51.459-51.46-79.8-119.879-79.8-192.654
        s28.34-141.194,79.8-192.654s119.879-79.8,192.654-79.8s141.194,28.34,192.654,79.8c51.46,51.46,79.8,119.879,79.8,192.654
        s-28.34,141.194-79.8,192.654S373.421,572.266,300.646,572.266z"/>
    <path d="M300.646,29.358c36.512,0,71.931,7.15,105.271,21.252c32.205,13.622,61.129,33.123,85.968,57.962
        c24.84,24.839,44.341,53.763,57.963,85.968c14.102,33.34,21.252,68.759,21.252,105.271c0,36.512-7.15,71.931-21.252,105.271
        c-13.622,32.205-33.123,61.129-57.963,85.968c-24.839,24.84-53.763,44.341-85.968,57.963
        c-33.34,14.102-68.759,21.252-105.271,21.252c-36.513,0-71.931-7.15-105.271-21.252c-32.205-13.622-61.129-33.123-85.968-57.963
        c-24.839-24.839-44.34-53.763-57.962-85.968c-14.102-33.34-21.252-68.759-21.252-105.271c0-36.513,7.15-71.931,21.252-105.271
        c13.622-32.205,33.123-61.129,57.962-85.968s53.763-44.34,85.968-57.962C228.715,36.509,264.133,29.358,300.646,29.358
         M300.646,25.358c-151.577,0-274.454,122.877-274.454,274.454c0,151.576,122.877,274.454,274.454,274.454
        c151.576,0,274.454-122.878,274.454-274.454C575.1,148.235,452.222,25.358,300.646,25.358L300.646,25.358z"/>
</g>
<g>
    <radialGradient id="SVGID_3_" cx="299.5176" cy="232.4409" r="259.4771" gradientUnits="userSpaceOnUse">
        <stop  offset="0" style="stop-color:#212121"/>
        <stop  offset="1" style="stop-color:#0D0D0D"/>
    </radialGradient>
    <path fill="url(#SVGID_3_)" d="M300.646,491.916c-105.927,0-192.104-86.178-192.104-192.104
        c0-105.927,86.178-192.104,192.104-192.104c105.926,0,192.104,86.178,192.104,192.104
        C492.75,405.738,406.572,491.916,300.646,491.916z"/>
    <path d="M300.646,109.708c25.666,0,50.561,5.026,73.996,14.938c22.638,9.575,42.968,23.283,60.429,40.743
        c17.46,17.46,31.167,37.791,40.742,60.428c9.912,23.435,14.938,48.331,14.938,73.996s-5.025,50.561-14.938,73.996
        c-9.575,22.638-23.282,42.968-40.742,60.429c-17.461,17.46-37.791,31.167-60.429,40.742c-23.435,9.912-48.33,14.938-73.996,14.938
        s-50.562-5.025-73.996-14.938c-22.637-9.575-42.968-23.282-60.428-40.742c-17.46-17.461-31.168-37.791-40.743-60.429
        c-9.912-23.435-14.938-48.33-14.938-73.996s5.026-50.562,14.938-73.996c9.575-22.637,23.283-42.968,40.743-60.428
        s37.791-31.168,60.428-40.743C250.084,114.733,274.98,109.708,300.646,109.708 M300.646,105.708
        c-107.201,0-194.104,86.903-194.104,194.104c0,107.201,86.903,194.104,194.104,194.104c107.201,0,194.104-86.903,194.104-194.104
        C494.75,192.611,407.847,105.708,300.646,105.708L300.646,105.708z"/>
</g>
<g opacity="0.8">
    <path id="color-temp" fill="#BE1E2D" opacity="0" d="M300.646,489.309c-104.489,0-189.497-85.008-189.497-189.497c0-104.489,85.008-189.497,189.497-189.497
        c104.489,0,189.497,85.008,189.497,189.497C490.143,404.301,405.135,489.309,300.646,489.309z"/>
    <path d="M300.646,110.815c25.515,0,50.266,4.997,73.566,14.852c22.505,9.519,42.718,23.146,60.075,40.504
        s30.984,37.57,40.504,60.075c9.854,23.3,14.852,48.051,14.852,73.566s-4.997,50.266-14.852,73.566
        c-9.52,22.505-23.146,42.718-40.504,60.075s-37.57,30.984-60.075,40.504c-23.3,9.854-48.051,14.852-73.566,14.852
        s-50.266-4.997-73.566-14.852c-22.505-9.52-42.718-23.146-60.075-40.504s-30.985-37.57-40.504-60.075
        c-9.855-23.3-14.852-48.051-14.852-73.566s4.997-50.266,14.852-73.566c9.519-22.505,23.146-42.718,40.504-60.075
        s37.57-30.985,60.075-40.504C250.38,115.812,275.131,110.815,300.646,110.815 M300.646,109.815
        c-104.933,0-189.997,85.064-189.997,189.997c0,104.932,85.064,189.997,189.997,189.997c104.932,0,189.997-85.064,189.997-189.997
        C490.643,194.879,405.578,109.815,300.646,109.815L300.646,109.815z"/>
</g>
<g id="temperature-markings">
        <line fill="none" stroke="#C7C8CA" stroke-width="5" stroke-miterlimit="10"
              x1="300.646" y1="123.751" x2="300.646" y2="161.603"/>
        <line fill="none" stroke="#C7C8CA" stroke-width="5" stroke-miterlimit="10"
              x1="422.102" y1="371.971" x2="454.881" y2="390.896"/>
        <line fill="none" stroke="#C7C8CA" stroke-width="5" stroke-miterlimit="10"
              x1="146.41" y1="390.896" x2="179.191" y2="371.97"/>
</g>
<g id="temp-transform" transform="translate(300,300)">
        <line id="current-reading" fill="none" stroke="#FFFFFF"
              stroke-width="5" stroke-linecap="round"
              stroke-miterlimit="10" x1="0" y1="-180" x2="0" y2="-120"/>
</g>
    <text transform="matrix(1 0 0 1 254.1387 83.3198)" fill="#D1D3D4" font-family="'Arial'"
          font-weight="bold" font-size="29.4616">thermo</text>
</svg>


</div>


<script type="text/javascript">

    (function () {

        window.onload = loaded;

        function loaded() {
            var colorTemp = document.getElementById("color-temp");
            var reading = document.getElementById('current-reading');
            var currentAngle = 0;
            var fill;

            var direction = 1;
            setInterval(function () {
                currentAngle += direction;
                if (currentAngle >= 120 || currentAngle <= -120) {
                    direction *= -1;
                } else if (currentAngle === 0) {
                    fill = direction === 1 ? "#BE1E2D" : "#10A2DC";
                    colorTemp.setAttribute("fill", fill);
                }
                // adjust the opacity
                colorTemp.setAttribute("opacity", Math.abs(currentAngle) / 120.0);

                reading.setAttribute("transform", "rotate(" + currentAngle + ")");

            }, 25);
        }
    })();


</script>
</body>
</html>

Nest Thermostat, Software Update 2.0

Nest recently released a new update to the software of the thermostat device (as well as their corresponding web and mobile applications).

Some of the details may be found on their blog.

A few of the new features include an historical view of the heating/cooling usage:

image

On Friday, April 6th for example, you can see when the heat turned on and what the set points were for the day for my First Floor thermostat. The data isn’t as interesting during our Midwest Spring as the furnace doesn’t run nearly as much.

Here’s from another day:

image

I doubt I’ll use this feature much. It only has 10 days of information available apparently right now, so I just can’t see this being very useful. I’m skeptical that this will affect my choices as it comes to how we use our HVAC system. I could see potentially how aggregate data of many users (in a similar geographical area) could become more compelling and potentially a source of data that Nest might be able to sell.

The settings for a thermostat have been tweaked visually. The same basic data is available as before:

image

The learning tab has been cleaned up as well:

image

For some reason, our thermostat that we’ve had for four months is apparently still in training (Time to Temp). That seems like an issue that maybe I’ll look into. Although I don’t really care much about the “time to temp” feature normally as I don’t manually adjust the affected thermostat much.

The “Away” tab changed:

image

Not a big improvement for usability. Probably more touch friendly (and it’s logically correct as it heats when less than 58 degrees for example), but it feels wrong. Thermostats aren’t normally left to right oriented (temp goes up and down), so this breaks a typical UX model.

On the Equipment tab, they’ve tweaked the UI as well:

image

I clicked on the Safety Temp word (? it’s not a button, nor a link, so I don’t know what to call it) and the above UI displayed. The same temperature range UX is displayed, but here I like it even less. I suppose we don’t have a maximum temperature in the house during cooling season, but this is clunky. (And given that it’s safety related, I wish it were more clear). I can hear some of you say, “but it’s clear to me.” I do understand it, but I’m confident there is a better way of displaying and adjusting these temperatures that would be more obvious.

(And Nest Labs, go ahead and spell out “TEMP” please? Thanks!)

The technical info tab is the same basically.

There’s now a lock feature (which I have no need for, and am not going to experiment with right now):

image

One of the big new TM’ed features is called Airwave™. Apparently, when it’s hot and the humidity is low (not typical for Wisconsin, as our summers are usually hot and humid), the thermostat apparently will try to do more cooling by turning off the air conditioning system early and using the fan more. (I always thought our air conditioner already did that as the compressor turns off before the fans). If it helps lower our electricity bill, awesome. I’ll report back if I can tell that it is working and helping (without historical data though, it will be difficult for us, especially as we added solar panels to our house last fall).

The scheduling tab looks basically unchanged. The support tab has more content, so you don’t have to go to their web site to read the information. That’s a nice improvement.


OK, this was very strange. As I was writing this post (and in the middle of using the application), I saw the following:

image

image

image

Now, the thermostats are all disconnected in some odd way:

image

A few minutes later, things improved (but not perfect):

image 

Twenty minutes later, the BASEMENT thermostat is still disconnected. I reset the thermostat and it’s back now.

You’re doing it wrong, alt text and title attributes

I had moved my mouse over an image on a local bank’s web site, and was surprised to see the name of the file pop-up.

SNAGHTML3867720c

The reason is that their web developer apparently doesn’t understand the meaning or intention behind the alt or title attribute of an HTML element.

Not that it isn’t handy to know the name of the file…. Smile

Maybe they could have used:

  • Passionate bank web site user
  • Man with loose tie
  • Man having good hair day
  • Or …. ?

(I noticed that they have this problem in many areas of their web site. Maybe their Banking skills are better than their web site skills?)

Paging Data with SQL Server Compact

image

If you’re using SQL Server Compact Editition version 4 or higher, there’s finally a decent and efficient way to do data paging:

var streets = db.Query<StreetName>(@"SELECT * FROM StreetNames
                   ORDER BY Id
                   OFFSET @offset ROWS
                   FETCH NEXT @rows ROWS ONLY",
                        new { offset = group * GroupSize, 
                            rows = GroupSize });

The code above uses Dapper-dot-net (with Contrib Extensions) to make the syntax of executing the Query succinct. (Recently, I’ve all but abandoned the Entity Framework in favor of Dapper).

The above code should be straightforward.

Given a table named, StreetNames created thusly:

db.Execute(@"CREATE TABLE StreetNames (          
            Id int IDENTITY NOT NULL,
            Name nvarchar(24) NOT NULL,
            CONSTRAINT pk_id PRIMARY KEY (Id))");

with some sample data inserted into the table:

foreach (var name in GetStreetNames())
{
    db.Execute("INSERT INTO StreetNames (Name) Values (@Name)",
        new { Name = name });
}

you’ll likely want to grab the data in pages. To get the data as pages, you’ll need to pick an index or something ordered (in this case, I used the Id column), specify the starting index and the total number to fetch:

OFFSET @offset ROWS
FETCH NEXT @rows ROWS ONLY

So, using the sample database:

using (var db = new SqlCeConnection(connectionString))
{
    db.Open();
    for (var group = 0; ; group++)
    {
        Console.WriteLine(string.Format("== GROUP {0} ==", group));
        var streets = db.Query<StreetName>(@"SELECT * FROM StreetNames
                           ORDER BY Id
                           OFFSET @offset ROWS
                           FETCH NEXT @rows ROWS ONLY",
                                new { offset = group * GroupSize, 
                                    rows = GroupSize });

        foreach (var street in streets)
        {
            Console.WriteLine(string.Format("{0} ({1})",
                street.Name, street.Id));
        }
        if (streets.Count() < GroupSize) { break; }
    }
}

The code loops until it can’t load a full GroupSize.

const int GroupSize = 8;

I added an extension method to IDbConnection to check whether a table exists:

public static class DbExtensions
{
    /// <summary>
    /// Determines whether the table exists
    /// </summary>
    /// <param name="connection">Existing, opened, database connection</param>
    /// <param name="tableName">The name of the table to test for.</param>
    /// <returns>True if table exists.</returns>
    public static bool TableExists(this IDbConnection connection, string tableName)
    {
        Debug.Assert(connection != null);
        Debug.Assert(!string.IsNullOrWhiteSpace(tableName));

        var cmd = connection.CreateCommand();
        cmd.CommandText = @"SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES 
                            WHERE TABLE_NAME=@TableName";
        var p1 = cmd.CreateParameter();
        p1.DbType = DbType.String;
        p1.ParameterName = "TableName";
        p1.Value = tableName;
        cmd.Parameters.Add(p1);

        var result = cmd.ExecuteScalar();
        return (int)result == 1;
    }
}

I really like the simple syntax it permits:

if (!db.TableExists("StreetNames"))

It’s used like this:

private static string CreateDatabase()
{
    var connectionString = string.Format("DataSource={0}", "test.sdf");
    SqlCeEngine engine = new SqlCeEngine(connectionString);
    try
    {
        engine.CreateDatabase();
    }
    catch (SqlCeException ex)
    {
        // file exists? (if so, we'll just ignore it ...)
        if (ex.NativeError != 25114) { throw; }
    }

    using (IDbConnection db = new SqlCeConnection(connectionString))
    {
        db.Open();
        // create a sample table 
        if (!db.TableExists("StreetNames"))
        {
            db.Execute(@"CREATE TABLE StreetNames (                
                        Id int IDENTITY NOT NULL,
                        Name nvarchar(24) NOT NULL,
                        CONSTRAINT pk_id PRIMARY KEY (Id))");
            foreach (var name in GetStreetNames())
            {
                db.Execute("INSERT INTO StreetNames (Name) Values (@Name)",
                    new { Name = name });
            }
        }
    }
    return connectionString;
}

Oddly, Sql Compact doesn’t throw proper (specific) exceptions, so you’ll be forced to check for NativeErrors or against strings rather than specific Exception types. (Seriously!)

Using the SqlCeEngine class, the code attempts to create a new Database file. If it already exists, it throws an exception. All but the file exists exception are rethrown.

Using an open connection and the TableExists extension, it creates a sample table called StreetNames, populated with this data:

private static IEnumerable<string> GetStreetNames()
{
    // Most common street names in UK, apparently :)
    yield return "High Street"; yield return "Station Road"; yield return "Main Street";
    yield return "Church Street"; yield return "Victoria Road"; yield return "Park Road";
    yield return "Church Road"; yield return "London Road"; yield return "Manor Road";
    yield return "New Road"; yield return "Park Avenue"; yield return "Queens Road";
    yield return "Kings Road"; yield return "Church Lane"; yield return "Green Lane";
    yield return "Alexandra Road"; yield return "The Crescent"; yield return "George Street";
    yield return "Grange Road"; yield return "Main Road"; yield return "King Street";
    yield return "The Avenue"; yield return "New Street"; yield return "North Street";
    yield return "Victoria Street"; yield return "West Street"; yield return "Queen Street";
    yield return "Springfield Road"; yield return "Stanley Road"; yield return "Albert Road";
    yield return "Albert Street"; yield return "Park Lane"; yield return "Chapel Street";
    yield return "Highfield Road"; yield return "The Green"; yield return "Mill Lane";
    yield return "Broadway"; yield return "St. Johns Road"; yield return "Marlborough Road";
    yield return "Windsor Road"; yield return "Forest Road"; yield return "South Street";
    yield return "Warwick Road"; yield return "Grove Road"; yield return "Kingsway";
    yield return "York Road"; yield return "Woodlands Road"; yield return "Clarence Road";
    yield return "School Lane"; yield return "Cromwell Road";
}

(I’d wanted to use the top 50 USA street names, but they’re far more boring as they’re like: Fourth Street, Fifth Street, Oak Street. Smile ).

There’s a tiny class to represent a StreetName that’s used later by the Dapper extension functions:

public class StreetName
{
    public int Id { get; set; }
    public string Name { get; set; }
}

The full main function:

static void Main(string[] args)
{
    const int GroupSize = 8;

    var connectionString = CreateDatabase();
    using (var db = new SqlCeConnection(connectionString))
    {
        db.Open();
        for (var group = 0; ; group++)
        {
            Console.WriteLine(string.Format("== GROUP {0} ==", group));
            var streets = db.Query<StreetName>(@"SELECT * FROM StreetNames
                               ORDER BY Id
                               OFFSET @offset ROWS
                               FETCH NEXT @rows ROWS ONLY",
                                    new { offset = group * GroupSize, 
                                        rows = GroupSize });

            foreach (var street in streets)
            {
                Console.WriteLine(string.Format("{0} ({1})",
                    street.Name, street.Id));
            }
            if (streets.Count() < GroupSize) { break; }
        }
    }

    Console.WriteLine("** Done **");
    Console.ReadKey();
}

The db.Query<StreetName> function is Dapper provided. Given the type and a Query, it automatically and efficiently constructs/deserializes objects of type StreetName for each row returned from the SQL query.

While the syntax isn’t quite as slick as  “Entity Framework” by any means, the resulting run-time efficiency far makes up for that as far as I’m concerned (and if you care about performance, you might want to re-evaluate your use of the EntityFramework as well).

I took the photo above years ago at the Boeing Museum of Flight in Seattle (which inspired me to look for videos). For fun, check out this video of a model SR-71 blackbird. It takes off around 2:21.

Model SR-71 Blackbird in flight.