28 March 2009

Chart in Javascript

There aren't enough bar-chart-drawing implementations out there yet, so this article will present the best one yet. It looks like this:

a chart made with javascript

Or several little charts in a row, like this:

several bar charts in a row

Features:

  • Really easy to use
  • mouse move highlights bar under mouse
  • pops up tooltip with information for the bar under the mouse
  • All colours are configurable
  • Scales data automatically to height of canvas
  • Calculates thickness of bars automatically so all fit in the width of the canvas
  • Redraw data as often as you want - for example with data from an ajax call

The tooltip is missing from this screenshot. My mac hides the tooltip before it takes the screenshot unfortunately.

Anyway, I'd like to convince you that it's really, really easy to use. Supply your data in JSON format like this:

var data = [
[ "week 1", "1000" ],
[ "week 2", "2000" ],
[ "week 3", "3000" ]
];

Declare a canvas element:

<canvas id="weekly_chart" width="200" height="80"></canvas>

Then just call the Chart(canvas_element, bar_colour, bg_colour, hilite_colour) constructor:

var chart = new Chart($('weekly_chart'), "rgb(128,128,255)", "rgb(0,0,0)", "rgb(255,255,128)");

Then, draw your data

chart.draw(data);

It couldn't be simpler!

Here's the code. Just include it somwhere in your page. It's free under a Creative Commons Attribution Share-Alike 3.0 license - you can use, re-use, modify, redistribute, as long as you link back to this page and (re)distributions carry the same or a compatible license. It has a slight dependency on Prototype (2 calls) but you jQuery people will fix that quickly (in fact, you might even leave a comment with the jQuery alternative). Theoretically, it will work on Internet Exploder using Google's excanvas, although I haven't tested this configuration.

(function() {
  function max(data) {
    var max = 0;
    for (var i = 0; i < data.length; i++) {
      if (data[i][1] > max) {
        max = data[i][1];
      }
    }
    return max;
  }

  function coords(event, element, f) {
    var offset = $(element).cumulativeOffset();   // $ from prototype
    var p = Event.pointer(event || window.event); // Event.pointer from prototype
    var y = p.y - offset.top;
    var x = p.x - offset.left;
    f(y, x);
  }

  window.Chart = function(canvas, fg, bg, hilite) {
    if (!canvas || !canvas.getContext) {
      return;
    }

    var cx = canvas.getContext('2d');

    this.draw = function(data) {
      cx.clearRect(0, 0, canvas.width, canvas.height);
      var thick = canvas.width / data.length;
      var scale = canvas.height / max(data);

      function highlightBars(index) {
        cx.lineWidth = 1;

        for (var i = 0; i < data.length; i++) {
          if (i == index || i == (index + 1)) {
            cx.strokeStyle = hilite;
          } else {
            cx.strokeStyle = bg;
          }
          cx.beginPath();
          cx.moveTo((i * thick) + 0.5, 0);
          cx.lineTo((i * thick) + 0.5, canvas.height);
          cx.stroke();
        }
      }

      cx.fillStyle = bg;
      cx.fillRect(0, 0, canvas.width, canvas.height);

      cx.fillStyle = fg;

      for (var i = 0; i < data.length; i++) {
        var h = data[i][1] * scale;
        if (!isNaN(h)) {
          cx.fillRect(i * thick, canvas.height - h, thick, h);
        }
      }

      highlightBars(-2);


      canvas.onmouseout = function(event) {
        highlightBars(-2);
      };

      canvas.onmousemove = function(event) {
        coords(event, canvas, function(y, x) {
          var index = ((x-1) / thick).floor();
          if (index < 0) {
            index = 0;
          }
          highlightBars(index);

          var bar = data[index];
          if (bar) {
            canvas.title = bar[0] + " : " + bar[1];
          }
        });
      };
    };
  };
})();

This is the simple version. Feel free to comment with improvements, I'll keep the code up to date with my favourite suggestions.

26 March 2009

Spidering Internal Pages

A url within an anchor tag in html may be absolute or relative. Absolute links look like <a href='http://iconfu.com'>iconfu ...</a> - they start with a protocol. Relative links look like <a href='bar.html'>bar info ...</a>. When you link to bar.html from http://example.com/pages/foo.html, the browser constructs the full reference and requests http://example.com/pages/bar.html.

So far, so good.

Relative links may also be of the form <a href='?browse=arrow'>arrow icons</a>. A browser requesting this link from http://iconfu.com/tags/list/0.html will construct this url: http://iconfu.com/tags/list/0.html?browse=arrow

This can be convenient when the code or script that handles the requested page is separate from the code or script that handles the request parameters. This doesn't happen often, but when it does, it's useful to be able to construct the url without needing to know the originating page. For example, a login handler might be implemented as a filter before the page is rendered, so the login request would simply be ?username=foo&password=bar ... this gets expanded by the browser into http://example.com/pages/foo.html?username=foo&password=bar. On the server, your login filter handles the login parameters, and your example/page script handles the rest of the url.

The bad news is that some spidering implementations handle this incorrectly (google's works fine). Instead of requesting http://iconfu.com/tags/list/0.html?browse=arrow from the earlier example, they request http://iconfu.com/tags/list?browse=arrow - they chop out "0.html". My code doesn't like this, and returns an error. Dumb MF spider implementations.

So that was that. Well, here's another bit of news: about 95% of visitors who come to iconfu through search, come from google. There are two ways to explain this: (1) google is the world's dominant search engine, who uses yahoo/live/ask.com anyway; (2) the clever people behind google analytics use some clever reporting techniques to show that google is the world's dominant search engine so why bother with the others.

We can eliminate (2) because as you know googlers Do No Evil. But today, in a flash of insight, I realised (3) perhaps those other search engines are sending me no visitors because they think my site is full of bugs and holes and 500 Internal Server Errors.

I'll fix that today and I'll let you know if I get a little more love from those unloved search engines. And then you can add "be careful with relative urls containing only a query string" to your SEO toolkit.

Open Coffee Club Paris: For Sale

Open Coffee Club, Paris, 26 March 2009

A dude is making a speech about legal issues for startups. I don't like this. I come to OCC for peer-to-peer networking, not go get lectured at. This is hijacking an open, social event to allow one person dominate and control the discussion. Not only that, but my conversation was interrupted! This wastes my time, because I am deprived of the ability to choose the people I want to converse with. I expect to meet people either because they are interesting or their service is useful to me. I have no respect for a dude who has effectively bought* OCC as a platform to market his services, and I have no respect for an OCC willing to sell itself in this way.

Some people are politely paying attention. Others look bored and are wondering when this abuse will be over. And I have nobody to talk to :((

* "bought" in the moral sense. I have no idea how the dude in question obtained authority to hijack the group.

20 March 2009

Scaling Testing

Check out TestSwarm by John Resig - it's like SETI@Home for distributed testing ... isn't that a totally awesome concept? If you're having difficulty scaling your tests, especially if browser and OS combinations are wearing out your head, this is worth a look ...

Updating the security budget

You might think in your cosy world of internal corporate web applications you don't need to worry about XSS and CSRF attacks (cross-site-scripting and cross-site request forgery) - after all, these are worries for public-facing web sites, not for us, surely?

Wrong!

Suppose your disgruntled employee leaves the project, and "in these times of crisis, you know", has nothing to do so gets scripting. Your disgruntled ex-employee, who previously worked on a precious sensitive internal system (aren't they all?), knows exactly which URLs will trigger a money transfer if accessed by persons with the right privileges, assuming they are logged in at the time.

Suppose that person is you - you're the manager of Whatsit and Whatnot after all, you have clearance for pretty much everything, and we used to do lunch together, so you'd happily open a link I sent you because it's sure to be interesting, entertaining, funny, informative, edifying ... you know, the kind of links I send to people.

But all I need to do is include this bit of code, which will be completely invisible to you:

<form id="maliciousForm" action="http://internal.bank.example.com/transferMoney" style="display:none;">
  <input type="hidden" name="from" value="myOldBossesAccount"/>
  <input type="hidden" name="to" value="myPersonalAccount"/>
</form>

<script>document.forms.maliciousForm.submit();</script>

If you happen to be logged in to http://internal.bank.example.com at the time (perhaps in another tab or window of your browser), that's all it takes to do the damage. To cover my tracks I'd need to be a lot smarter and more subtle, but the basic attack is trivial. And, more likely than not, your application isn't protected.

In fact, you don't even know what scripts are embedded in this page ... do you dare "view source" and check? What malicious clandestine script has executed while you were reading this paragraph?

Time to run to the program manager and get a security budget extension! While you're at it, please don't shoot the messenger: smarter and meaner people than I (yes, they exist) have already thought of this. And even if your disgruntled ex-employee isn't that smart or mean, he might well be willing to sell his knowledge to someone who is.

For more information, see Adam Barth, Collin Jackson, and John C. Mitchell, Robust Defenses for Cross-Site Request Forgery (pdf). The most reliable defence involves ensuring all potentially harmful requests are made via POST, not GET; and then requiring that any POST request includes a secret token in its body (possibly via a hidden input); the server validates the secret token and allows the request to proceed only if the token is valid. Ruby on Rails provides framework methods for simplifying this (seriously: you call the protect_from_forgery method).

Asking your corporate users to logout when they're not using your precious sensitive application is like asking dogs to stop wagging their tails. It's not going to happen. If security isn't your problem, then it's a problem.

This post was inspired by the "Web application security horror stories" talk at FOWA Dublin 2009 by Simon Willison

17 March 2009

Jamendo & Magnatune

Thanks to Laurent I found Jamendo, a distributor of Creative-Commons-licensed music, and from there Magnatune, with similar goals. Magnatune artists get 50% of proceeds from sales of their work. Here's a Bach cantata from Magnatune:

JS Bach Cantatas - Volume IV - Early Cantatas for Holy Week by American Bach Soloists

The quality of the music is good, but there's an annoying voice between each track that tells you the album and artist name, which you knew already. I hope the paid version excludes this.

I'm going to explore these sites some more. It might be time to bid farewell to the iTunes Store, and all those Big Record Labels and their fascist anti-piracy methods with it.

10 March 2009

Ban Censorship Now!

An Irish ISP, eircom, has agreed to ban any website the IRMA (Irish Recorded Music Association) chooses to block.

I had thought censorship was one of those nasty deals you get in China and Iran, but now it's thriving in Ireland. Please support http://blackoutireland.com/ even if you're not Irish. I hate to fearmonger, but this is coming to an ISP near you, wherever you are, in the near future if it's not stopped now.

Personally, I'm not interested in illegal music (my musical preferences aren't the kind that go with P2P), but it is not acceptable for a private corporation to decide what sites I may or may not use. Now I just feel all icky and horrible when I enter a music store. All those shiny CDs are whispering "censorship, censorship" at me.

Go and prosecute the criminals, leave the rest of us alone!

03 March 2009

Blogger: break "convert line breaks"

Blogger's "convert line breaks" setting seems to cause a lot of pain, and what's more, it doesn't even seem to work. I get gratuitous <br/> in my code despite having set this setting to OFF. The issue and some workarounds are discussed on Rob on Programming, The Real Blogger Status, and MLA Wire.

The trouble is, if you've chosen to edit in "Edit Html" mode, it's reasonable to suppose that you know what you're doing, and you want the html code of your post to be exactly what you write. In other words, I can take care of my own <p> and <br> tags.

Blogger doesn't think so. And here's my revenge - I added this to the "style" section of my template:

  br { display: none; }
  br.forReal { display:inline; }

This way, I never have to think about Blogger's thoughtful, kind, but misguided insertion of line breaks again. And when I want a line break for real, which isn't very often (I'm more of a <p>...</p> person), I just

  <br class='forReal'/>

So I can make a table thusly*:

<table style="width:auto;" cellpadding="1" cellspacing="1" border="1">
  <tr>
    <td>foo</td>
    <td>bar</td>
  </tr>
  <tr>
    <td>toto</td>
    <td>titi</td>
  </tr>
</table>

Which looks like

foo bar
toto titi

Slight problem: it has probably gone and damaged all my old posts. What a pain!

* I know, "thusly" isn't a word. I don't care.