development

Dialup still matters ... OR ... how to ruin someone's day with a couple bits of code, and how to fix it

I recently relaunched the Mars Odyssey THEMIS web site, moving from a home-grown Perl not-really-a-CMS to Drupal. There were a number of technical challenges moving from a site and architecture that had grown organically over many years to a modern CMS with better usability from both the end users and site managers perspective. Talking about those challenges is for another post, but for now, a reminder how important small decisions can be to usability.

I received this email this morning:

Our school network is old and slow. Under the old display system I could display part of the image as it came in and talk about it with the students, holding their attention while it slowly came in. Under the new system all we get is a rotating disk for 1 to 5 minutes depending on our network's condition. This time is a total waste making the site useless. By the time the image has come in I've lost the students.

I no longer use this site which I did use daily as it no longer works with students (elementary) short attention spans. Other sites are now a better use of our limited time.

I thought you needed to know this. Maybe it works for schools with more modern networks. But with our budget shrinking rapidly we are stuck with this one for the next few years. To us it is a step backwards.

In my haste to get the site up, I'd overlooked a very important part of site usability - with a graphically intensive site like THEMIS where you can browse through thousands of "Images of the Day" mission planners have posted over the years, something as minor as choosing what image format to serve can make a huge difference to some users.

In this case, the teacher was used to how JPEGs worked - downloading slowly but progressively while he talked to his students. The new site served images from a proprietary image server rather than caching them on disk, and by default, was sending out PNG's, which in some browsers, take much longer to load.

The solution? Adding pair of query string parameters to my image server: format=jpeg&quality=60. This cut the overall download time and made the images load faster in the browser. Luckily this was all themed content in Drupal so it was a simple chore to fix the template output.

To test my solution, I turned to a simple method of throttling down my bandwidth. With the Mac OS, you can configure the firewall using IPFW to limit your inbound or outbound bandwidth on specific ports. Here's how:

From the Terminal, set up the rule:

sudo ipfw pipe 1 config bw 10KByte/s plr 0.1 delay 50ms

(You can vary the latency and bandwidth to test different scenarios)

Apply the rule to inbound traffic on port 80 only:

sudo ipfw add 1 pipe 1 dst-port 80

Do your testing.

And when you're finished testing, remove the rule:

sudo ipfw delete 1

You can manage your firewall rules with a host of graphical apps as well if you don't want to venture into the Terminal, but it's pretty quick and basic.

UI responsiveness is an under-appreciated part of the overall user experience in applications for many developers and designers (and a part that the big boys like Google and Facebook spend plenty of effort on), but it can make a huge difference to users. It doesn't just affect classrooms in rural northern Minnesota - responsiveness is vital to the success of mobile apps as well, where user patience levels are even lower than on the desktop.


Displaying multiple axes for a single series in Flex charts

I'm building a Flex application for work used to manage and manipulate infrared spectroscopy data.

Typically, this data is plotted like so, using Gnuplot or some similar plotting package.

Where one or more series are plotted where X values are wavelength/wavenumber and Y values are emissivities.

The two X-axes shown are simply different representations of the same scale where wavelength = 1/wavenumber.

I was stumped as to how to render a chart with two axes for the same data, as the more common use case for multiple axes in Flex charting is to render multiple series with entirely different scales on the same chart.

It occurred to me then that while it's not possible to have multiple horizontalAxis declarations in the same chart, there's no reason you can use more than one AxisRenderer per axis.

There were three keys to duplicating the original plot in Flex, noted in the code comments below.

In the MXML:

<mx:horizontalAxisRenderers>
  <!--
        Adding multiple axes for the same lineseries is as
        simple as adding multiple AxisRenderers with different
        placement parameters.
  -->
  <mx:AxisRenderer axis="{primaryXaxis}" labelFunction="{primaryXaxisLabelFunc}" placement="bottom"/>
  <mx:AxisRenderer axis="{primaryXaxis}" labelFunction="{secondaryXaxisLabelFunc}" placement="top">
        <!--
            Because we want different titles for each X-axis we're
            using a titleRenderer. Here all we want to do is change
            the text of the title, so we're using an inline renderer
            but you could subclass the ChartLabel class to create an
            external renderer for more extensive changes.
        --> 
      <mx:titleRenderer>
          <mx:Component>
              <mx:Label text="Wavelength (μm)"/>
          </mx:Component>
      </mx:titleRenderer>
  </mx:AxisRenderer>
</mx:horizontalAxisRenderers>

And then to manipulate the labels for each axis, we create a custom labelFunction

/**
* To set the secondary X-axis to a different scale using the
* same data, we're applying a scaling factor to the label and
* returning it. We're also inverting as we do with the primary
* X-axis.
* 
* To convert from wavenumber in cm-1 to wavelength in μm the
* formula is wavelength = 10000/wavenumber
*/
private function secondaryXaxisLabelFunc(axisrenderer:IAxisRenderer, label:String):String
{
	var labelAsNumber : Number = Number(label);
 
    if (isNaN(labelAsNumber))
        return label;
 
    return (axisFormatter.format(10000/labelAsNumber * -1));
}

And the result (right click to view complete source).

Multi-column lists with jQuery, an alternative method

So I needed a method to take a long, nested list and turning it into a compact, multiple acolumn list, in order to display it as sort of a site map for the home page for a site I'm working on.

Being a huge fan of jQuery, it was naturally my go-to library of choice.

Scanning the plugins site, I found a possible solution from a feller called Ingo Schommer called columnizeList.

Score, right? Well... not exactly, at least for my case.

Ingo used some of the methodoligies outlined in this article on multi-column lists on A List Apart. One of the caveats of his methodology is that each list item has to be the same height. This works Ok for a lot of use cases, but since I'm using a Drupal menu as the source for the list, it could contain arbitrary text I don't control.

So, I started from scratch. Instead of relying on consistent line heights, and applying different margin settings to list elements, I instead decided to decompose the large source list into several smaller lists (one for each column) and then use a css float parameter to make them all appear side-by-side.

Here's a sample list for a demonstration, cribbed from Ingo's example:

  1. harold (3550)
  2. horatio (1320)
  3. hitler (1120)
  4. henry (784)
  5. hector (358)
  6. haploid (315)
  7. hopping (50)
  8. herbert mulroney (44)
  9. hopscotching (29)
  10. hominibus (19)
  11. honkey (19)
  12. hermoine (18)
  13. hieronymus (13)
  14. halliburton (12)
  15. hummer (10)
  16. harlod (10)
  17. heironymious (9)
  18. hemorrhoids (7)
  19. hammersack (6)

(apparently a list of the most common fillers for the middle initial in Jesus H. Christ)

Anyway, here's what my script does to the above list:

  1. harold (3550)
  2. horatio (1320)
  3. hitler (1120)
  4. henry (784)
  5. hector (358)
  6. haploid (315)
  7. hopping (50)
  8. herbert mulroney (44)
  9. hopscotching (29)
  10. hominibus (19)
  11. honkey (19)
  12. hermoine (18)
  13. hieronymus (13)
  14. halliburton (12)
  15. hummer (10)
  16. harlod (10)
  17. heironymious (9)
  18. hemorrhoids (7)
  19. hammersack (6)

And here's the code:


/*
Copyright (c) 2007 Christian yates
christianyates.com
chris [at] christianyates [dot] com
Licensed under the MIT License: 
http://www.opensource.org/licenses/mit-license.php
 
Inspired by work of Ingo Schommer
http://chillu.com/2007/9/30/jquery-columnizelist-plugin
*/
(function($){
  $.fn.columnizeList = function(settings){
    settings = $.extend({
      cols: 3,
      constrainWidth: 0
    }, settings);
    // var type=this.getNodeType();
    var container = this;
    if (container.length == 0) { return; }
    var prevColNum = 10000; // Start high to avoid appending to the wrong column
    var size = $('li',this).size();
    var percol = Math.ceil(size/settings.cols);
    var tag = container[0].tagName.toLowerCase();
    var classN = container[0].className;
    var colwidth = Math.floor($(container).width()/settings.cols);
    var maxheight = 0;
    // Prevent stomping on existing ids with pseudo-random string
    var rand = Math.floor(Math.random().toPrecision(6)*10e6);
    $('<ul id="container'+rand+'" class="'+classN+'"></ul>').css({width:$(container).width()+'px'}).insertBefore(container);
    $('li',this).each(function(i) {
      var currentColNum = Math.floor(i/percol);
      if(prevColNum != currentColNum) {
        if ($("#col" + rand + "-" + prevColNum).height() > maxheight) { maxheight = $("#col" + rand + "-" + prevColNum).height(); }
        $("#container"+rand).append('<li class="list-column-processed"><'+tag+' id="col'+rand+'-'+currentColNum+'"></'+tag+'></li>');
      }
      $(this).attr("value",i+1).appendTo("#col"+rand+'-'+currentColNum);
      prevColNum = currentColNum;
    });
    $("li.list-column-processed").css({
      'float':'left',
      'list-style':'none',
      'margin':0,
      'padding':0
    });
    if (settings.constrainWidth) {
      $(".list-column-processed").css({'width':colwidth + "px"});
    };
    $("#container"+rand).after('<div style="clear: both;"></div>');
    $("#container"+rand+" "+tag).height(maxheight);
    // Add CSS to columns
    this.remove();        
    return this;
  };
})(jQuery);

Download

There are only two parameters - cols, the number of columns to break the list into, and constrainWidth a boolean (defaulting to false) to specify whether you want all columns to be the same width.

I've tested with IE 6&7, FF3, Safari3 and Opera 9.something (for the three Opera users on the planet). The code could use a bit of refactoring perhaps for the purpose of beautification.

Update: I've added this to the jQuery Plugin site.

High productivity

Have I mentioned that I loves me some Drupal?

Oh yes. I probably have. Anyway, in the last 48 hours of last week I managed to take two sites from photoshop mockup to fully functioning. Well, almost, anyway. There are of course some minor issues with Internet Exploder to deal with, and some pages to be tweaked.

We created a common template architecture for niche sites that can rapidly be reskinned with CSS to create unique sites with minimal code changes. The rest, from the Heritage site course guide to the Mom2MomSC.com multimedia sharing page was implemented with a minimum of coding, and in very short order using community-contributed Drupal modules. We plugged in our proprietary Omniture analytics module, Vmix video integration and my own Videowrapper module to round out the sites.

I'm a fan of more abstracted frameworks like Ruby on Rails, Django and the like for highly specialized vertical applications, but for a rapid time-to market general purpose application platform it's hard to beat Drupal, where most of the common hooks like authentication, access control, content organization, are already in place, and hundreds (thousands) of other high-quality modules are just a click away. And if you want to plug in your own alternate solutions (like we have for integration with a proprietary corporate registration system), you can do that too. So basically, you can concentrate on features that achieve your business goals, rather than the ancillary elements.

What would you rather do? Spend your time an energy reinventing the wheel, or building revenue and audience generating products?

IMHO, which is worth what you paid for it, I'd rather run something that got me 95% of the way on a project and let me spend most of my time on the remaining edge cases, optimization and security than spend 95% of my time just getting the framework built to support whatever business goals I have.


Getting things done without GTD, being agile without Agile

Or, "does the process matter more than the work?"

Maybe I've been in my isolated little work world for too long, where I've had to wear lots of hats, and for the most part, teach myself everything I've needed to know, and just get things done. But apparently that's not important these days. These days, you need to GTD. And agility in your process is not enough, you have to be Agile, and Scrum.

What's all this? Well it seems to me that the process of how work is done, especially software development work, is more important to some people than the work itself.

I've been dismissed from interviews because I couldn't describe the Scrum process, yet I've worked iteratively on software projects, delivered features on deadline, and added features over time. Hmm.

I've read job requirements that include "Must follow GTD process and be Agile". Hmm.

I've watched 'professional developers' fritter away time and money talking about their code sprints while not actually delivering any product (or even product components). Hmm.

I guess it all looks good on a resume, and I may be naive, but when did concentrating time and energy (and money!) on the process become more important than concentrating on building the product/service? When did buzzword compliance become the key criteria for evaluating employee potential?

Where do you draw the line between the effort you spend on organizing your time and the effort you spend actually moving work forward?

Flame me if you must.