<?xml version="1.0" encoding="UTF-8"?>
<commit>
  <added type="array">
    <added>
      <filename>.gitignore</filename>
    </added>
    <added>
      <filename>test/normalisation.html</filename>
    </added>
    <added>
      <filename>test/test.css</filename>
    </added>
    <added>
      <filename>test/unittest.js</filename>
    </added>
  </added>
  <modified type="array">
    <modified>
      <diff>@@ -25,12 +25,14 @@ These goals are based on recommendations in Stephen Few's books:
 
 Which was generally in turn based on Edward Tufte's work.
 
+h3. Test-Driven
+
+I've been rebuilding sections of the library with &lt;code&gt;unittest.js&lt;/code&gt; from scriptaculous.  Unit tests can be run in a browser by loading the HTML files.  I'm also using these tests to help profile the library and improve performance. 
+
 h3. Todo
 
-* IE bugs: text alignment for none centre labels (Raphael/VML issue), sparklines aren't inline
-* Label wrapping
-* Wrap wide label text in boxes
-* Determine library structure
+* Remove JavaScript library dependency
+* Package more normalisation code
 * Humanize labels: rather than showing 1,000,000 optionally show &quot;1m&quot;
 
 h2. Examples</diff>
      <filename>README.textile</filename>
    </modified>
    <modified>
      <diff>@@ -1,6 +1,8 @@
 var Ico = {
   Base: {},
-  
+
+  Normaliser: {},
+
   SparkLine: {},
   SparkBar: {},
 
@@ -10,31 +12,94 @@ var Ico = {
   HorizontalBarGraph: {}
 }
 
-Ico.Base = Class.create({
-  /* Returns a suitable set of labels for given data points on the Y axis */
-  labelStep: function(data) {
-    var min = data.min(),
-        max = data.max(),
-        range = max - min,
-        step = 0;
-
-    if (range &lt; 2) {
-      step = 0.1;
-    } else if (range &lt; 3) {
-      step = 0.2;
-    } else if (range &lt; 5) {
-      step = 0.5;
-    } else if (range &lt; 11) {
-      step = 1;
-    } else if (range &lt; 50) {
-      step = 5;
-    } else if (range &lt; 100) {
-      step = 10;
-    } else {
-      step = Math.pow(10, (Math.log(range) / Math.LN10).round() - 1);
+/* Supporting methods to make dealing with arrays easier */
+/* Note that some of this work to reduce framework dependencies */
+Array.prototype.sum = function() {
+	for (var i = 0, sum = 0; i &lt; this.length; sum += this[i++]);
+	return sum;
+}
+
+if (typeof Array.prototype.max == 'undefined') {
+  Array.prototype.max = function() {
+    return Math.max.apply({}, this)
+  }
+}
+
+if (typeof Array.prototype.min == 'undefined') {
+  Array.prototype.min = function() {
+    return Math.min.apply({}, this)
+  }
+}
+
+Array.prototype.mean = function() {
+  return this.sum() / this.length;
+}
+
+Array.prototype.variance = function() {
+  var mean = this.mean(),
+      variance = 0;
+  for (var i = 0; i &lt; this.length; i++) {
+    variance += Math.pow(this[i] - mean, 2);
+  }
+  return variance / (this.length - 1);
+}
+
+Array.prototype.standard_deviation = function() {
+  return Math.sqrt(this.variance());
+}
+
+Ico.Normaliser = Class.create({
+  initialize: function(data, options) {
+    this.options = {
+      start_value: null
+    };
+    Object.extend(this.options, options || { });
+
+    this.min = data.min();
+    this.max = data.max();
+    this.standard_deviation = data.standard_deviation();
+    this.range = 0;
+    this.step = 1;
+    this.start_value = this.calculateStart();
+    this.process();
+  },
+
+  calculateStart: function(offset) {
+    offset = offset || 1;
+    var start_value = this.round(this.options.start_value != null &amp;&amp; this.min &gt;= 0 ? this.options.start_value : this.min, offset);
+    if (start_value &lt; 0 &amp;&amp; start_value &gt; this.min) {
+      return this.calculateStart(offset + 1);
     }
-    
-    return step;
+    return start_value;
+  },
+
+  /* Given a value, this method rounds it to the nearest good value for an origin */
+  round: function(value, offset) {
+    offset = offset || 1;
+    if (this.standard_deviation &gt; 0.1) {
+      var multiplier = Math.pow(10, length - offset);
+      value *= multiplier;
+      value = Math.round(value) / multiplier;
+    }
+
+    return value;
+  },
+
+  process: function() {
+    this.range = this.max - this.start_value;
+    this.step = this.labelStep();
+  },
+
+  labelStep: function() {
+    return Math.pow(10, (Math.log(this.range) / Math.LN10).round() - 1)
+  }
+});
+
+Ico.Base = Class.create({
+  normaliseData: function(data) {
+    return $A(data).collect(function(value) {
+      return this.normalise(value);
+    }.bind(this))
   }
 });
 
@@ -67,19 +132,13 @@ Ico.SparkLine = Class.create(Ico.Base, {
   calculateStep: function() {
     return this.options['width'] / (this.data.length - 1);
   },
-  
-  normalisedData: function() {
-    return $A(this.data).collect(function(value) {
-      return this.normalise(value);
-    }.bind(this))
-  },
-  
+
   normalise: function(value) {
     return (this.options['height'] / this.data.max()) * value;
   },
   
   draw: function() {
-    var data = this.normalisedData();
+    var data = this.normaliseData(this.data);
     
     this.drawLines('', this.options['colour'], data);
     
@@ -136,13 +195,12 @@ Ico.BaseGraph = Class.create(Ico.Base, {
 
     this.data_sets = Object.isArray(data) ? new Hash({ one: data }) : $H(data);
     this.flat_data = this.data_sets.collect(function(data_set) { return data_set[1] }).flatten();
-    this.range = this.calculateRange();
-    this.data_size = this.longestDataSetLength();
-    this.start_value = this.calculateStartValue();
 
-    if (this.start_value == 0) {
-      this.range = this.max;
-    }
+    this.normaliser = new Ico.Normaliser(this.flat_data, this.normaliserOptions());
+    this.label_step = this.normaliser.step;
+    this.range = this.normaliser.range;
+    this.start_value = this.normaliser.start_value;
+    this.data_size = this.longestDataSetLength();
 
     /* If one colour is specified, map it to a compatible set */
     if (options &amp;&amp; options['colour']) {
@@ -181,17 +239,16 @@ Ico.BaseGraph = Class.create(Ico.Base, {
     
     this.graph_width = this.options['width'] - (this.x_padding);
     this.graph_height = this.options['height'] - (this.y_padding);
-    
+
     this.step = this.calculateStep();
-    this.label_step = this.labelStep(this.flat_data);
 
     /* Calculate how many labels are required */
     this.y_label_count = (this.range / this.label_step).round();
+    if ((this.normaliser.min + (this.y_label_count * this.normaliser.step)) &lt; this.normaliser.max) {
+      this.y_label_count += 1;
+    }
     this.value_labels = this.makeValueLabels(this.y_label_count);
     this.top_value = this.value_labels.last();
-    if (this.start_value == 0) {
-      this.range = this.top_value;
-    }
 
     /* Grid control options */
     this.grid_start_offset = -1;
@@ -208,6 +265,10 @@ Ico.BaseGraph = Class.create(Ico.Base, {
     this.setChartSpecificOptions();
     this.draw();
   },
+
+  normaliserOptions: function() {
+    return {};
+  },
   
   chartDefaults: function() {
     /* Define in child class */
@@ -225,11 +286,6 @@ Ico.BaseGraph = Class.create(Ico.Base, {
     /* Define in child classes */
   },
   
-  calculateStartValue: function() {
-    var min = this.flat_data.min();
-    return this.range &lt; min || min &lt; 0 ? min.round() : 0;
-  },
-  
   makeRandomColours: function(number) {
     var colours = {};
     this.data_sets.each(function(data) {
@@ -274,19 +330,6 @@ Ico.BaseGraph = Class.create(Ico.Base, {
     return this.options['font_size'];
   },
   
-  /* Subtract the largest and smallest values from the data sets */
-  calculateRange: function() {
-    this.max = this.flat_data.max();
-    this.min = this.flat_data.min();
-    return this.max - this.min;
-  },
-  
-  normaliseData: function(data) {
-    return $A(data).collect(function(value) {
-      return this.normalise(value);
-    }.bind(this))
-  },
-  
   normalise: function(value) {
     var total = this.start_value == 0 ? this.top_value : this.range;
     return ((value / total) * (this.graph_height));
@@ -437,7 +480,6 @@ Ico.BaseGraph = Class.create(Ico.Base, {
       this.paper.text(x + font_offsets[0], y - font_offsets[1], label).attr(font_options).toFront();
       x = x + x_offset(step);
       y = y + y_offset(step);
-
     }.bind(this));
   },
   
@@ -451,7 +493,6 @@ Ico.BaseGraph = Class.create(Ico.Base, {
   }
 });
 
-
 Ico.LineGraph = Class.create(Ico.BaseGraph, {
   chartDefaults: function() {
     return { plot_padding: 10 };
@@ -459,7 +500,7 @@ Ico.LineGraph = Class.create(Ico.BaseGraph, {
 
   setChartSpecificOptions: function() {
     if (typeof(this.options['curve_amount']) == 'undefined') {
-      this.options['curve_amount'] = 10
+      this.options['curve_amount'] = 10;
     }
   },
   
@@ -489,11 +530,14 @@ Ico.LineGraph = Class.create(Ico.BaseGraph, {
   }
 });
 
-/* This is based on the line graph, I can probably inherit from a shared class here */
 Ico.BarGraph = Class.create(Ico.BaseGraph, {
   chartDefaults: function() {
     return { plot_padding: 0 };
   },
+
+  normaliserOptions: function() {
+    return { start_value: 0 };
+  },
   
   setChartSpecificOptions: function() {
     this.bar_padding = 5;
@@ -528,11 +572,10 @@ Ico.BarGraph = Class.create(Ico.BaseGraph, {
   }
 });
 
-/* This is based on the line graph, I can probably inherit from a shared class here */
 Ico.HorizontalBarGraph = Class.create(Ico.BarGraph, {
   setChartSpecificOptions: function() {
     // Approximate the width required by the labels
-    this.x_padding_left = 12 + this.longestLabel() * (this.options['font_size'] / 2);
+    this.x_padding_left = 20 + this.longestLabel() * (this.options['font_size'] / 2);
     this.bar_padding = 5;
     this.bar_width = this.calculateBarHeight();
     this.options['plot_padding'] = 0;</diff>
      <filename>ico.js</filename>
    </modified>
    <modified>
      <diff>@@ -63,6 +63,24 @@ p.example { color: #555; }
     &lt;div id=&quot;linegraph_negative_and_positive&quot; class=&quot;linegraph&quot;&gt;&lt;/div&gt;
     &lt;div id=&quot;bar_negative_and_positive&quot; class=&quot;linegraph&quot;&gt;&lt;/div&gt;
     &lt;div id=&quot;horizontal_negative_and_positive&quot; class=&quot;linegraph&quot;&gt;&lt;/div&gt;
+
+    &lt;h2&gt;Contributed Examples&lt;/h2&gt;
+
+    &lt;p&gt;Min and max are the same:&lt;/p&gt;
+    &lt;div class=&quot;linegraph&quot; id=&quot;rics_poll&quot;&gt;&lt;/div&gt;
+
+    &lt;p&gt;Small range difference, but should still start at 0:&lt;/p&gt;
+    &lt;div class=&quot;linegraph&quot; id=&quot;rics_poll4&quot;&gt;&lt;/div&gt;
+
+    &lt;p&gt;Small range difference, but should still start at 0 (it's a bar chart):&lt;/p&gt;
+    &lt;div class=&quot;linegraph&quot; id=&quot;rics_poll5&quot;&gt;&lt;/div&gt;
+
+    &lt;p&gt;Large min max set to the same:&lt;/p&gt;
+    &lt;div class=&quot;linegraph&quot; id=&quot;rics_poll3&quot;&gt;&lt;/div&gt;
+
+    &lt;p&gt;Negative values should always be above the axis (not fixed yet):&lt;/p&gt;
+    &lt;div class=&quot;linegraph&quot; id=&quot;rics_poll2&quot;&gt;&lt;/div&gt;
+
     &lt;script type=&quot;text/javascript&quot;&gt;
 var sparkline = new Ico.SparkLine($('sparkline'), [21, 41, 32, 1, 10, 5, 32, 10, 23], { width: 30, height: 14, background_colour: '#ccc' });
 var sparkline_2 = new Ico.SparkBar($('sparkline_2'), [1, 5, 10, 15, 20, 15, 10, 15, 30, 15, 10], { width: 30, height: 14, background_colour: '#ccc' });
@@ -76,7 +94,7 @@ var linegraph_2 = new Ico.LineGraph($('linegraph_2'), [100, 10, 90, 20, 80, 30,
 var linegraph_3 = new Ico.LineGraph($('linegraph_3'), $R(1, 25).collect(function() { return (Math.random() * 10000).round() } ));
 var linegraph_4 = new Ico.LineGraph($('linegraph_4'), $R(1, 25).collect(function() { return Math.random() } ));
 
-var linegraph_5 = new Ico.LineGraph($('linegraph_5'),{shoe_size: [11,10,5]},{ markers: 'circle', colours: {shoe_size: '#990000' }, grid: true });
+var linegraph_5 = new Ico.LineGraph($('linegraph_5'),{shoe_size: [15,14,10]},{ markers: 'circle', colours: {shoe_size: '#990000' }, grid: true });
 
 var bargraph_5 = new Ico.HorizontalBarGraph($('bargraph_5'), [2, 5, 1, 10, 15, 33, 20, 25, 1], { font_size: 20, labels: ['label one', 'label two', 'label three', 'four', 'five', 'six', 'seven', 'eight', 'nine'], colour: '#ff0099' });
 var bargraph_6 = new Ico.BarGraph($('bargraph_6'), [2, 5, 1, 10, 15, 33, 20, 25, 1], { labels: ['label one', 'label two', 'label three', 'label four', 'label five', 'label six', 'label seven', 'label eight', 'label nine'] });
@@ -88,7 +106,12 @@ var linegraph_med_high = new Ico.LineGraph($('linegraph_med_high'), $R(1, 25).co
 var linegraph_negative = new Ico.LineGraph($('linegraph_negative'), [57,-31,-87,-66,-30,-77,-88,75,-25,48,-56,-91,16,-41,87,69,65,-62,-58,15,-49,-75,42,-78,-79]);
 var linegraph_negative_and_positive = new Ico.LineGraph($('linegraph_negative_and_positive'), [-57,-31,-87,-66,-30,-77,-88,-75,-25,-48,-56,-91,-16,-41,-87,-69,-65,-62,-58,-15,-49,-75,-42,-78,-79]);
 var bar_negative_and_positive = new Ico.BarGraph($('bar_negative_and_positive'), [-57,-31,-87,66,-30,-77,-88,-75,-25,-48,-56,-91,16,-41,-87,-69,-65,-62,58,-15,-49,-75,-42,-78,-79]);
-var horizontal_negative_and_positive = new Ico.HorizontalBarGraph($('horizontal_negative_and_positive'), [-57,-31,-87,66,-30,-77,-88,-75,-25,-48,-56,-91,16,-41,-87,-69,-65,-62,58,-15,-49,-75,-42,-78,-79]);
+var horizontal_negative_and_positive = new Ico.HorizontalBarGraph($('horizontal_negative_and_positive'), [-57,-31,-87,66,-30,-77,-88,-75,-20,-48,-56,-91,16,-41,-87,-69,-65,-62,58,-15,-49,-75,-42,-78,-79]);
+var rics_poll = new Ico.BarGraph($('rics_poll'), [20, 20],  { font_size: 10, colour: '#ff0099', labels: ['label one', 'label two'] });
+var rics_poll = new Ico.BarGraph($('rics_poll4'), [20, 19, 15, 10],  { font_size: 10, colour: '#ff0099', labels: ['label one', 'label two'] });
+var rics_poll = new Ico.BarGraph($('rics_poll5'), [10.1, 10.2, 10.5, 11],  { font_size: 10, colour: '#ff0099' });
+var rics_poll2 = new Ico.BarGraph($('rics_poll2'), [20, -20, -30],  { font_size: 10, colour: '#ff0099', labels: ['label one', 'label two'] });
+var rics_poll2 = new Ico.BarGraph($('rics_poll3'), [2000, 2000],  { font_size: 10, colour: '#ff0099', labels: ['label one', 'label two'] });
     &lt;/script&gt;
   &lt;/body&gt;
 &lt;/html&gt;</diff>
      <filename>index.html</filename>
    </modified>
  </modified>
  <removed type="array"/>
  <parents type="array">
    <parent>
      <id>bcddc0f7a33ca1f9cae19f609aaba64ef105f375</id>
    </parent>
  </parents>
  <author>
    <name>Alex Young</name>
    <email>alex@helicoid.net</email>
  </author>
  <url>http://github.com/alexyoung/ico/commit/6a333cc9e26e38713aae8e52dc61065351e59dd3</url>
  <id>6a333cc9e26e38713aae8e52dc61065351e59dd3</id>
  <committed-date>2009-07-20T02:31:45-07:00</committed-date>
  <authored-date>2009-07-20T02:31:45-07:00</authored-date>
  <message>Added new normalisation class and unit tests.  This version fixes issues with range zooming to make it a bit more robust.  It also forces all bar charts to start at 0 unless they have negative values.  This makes sense because bar charts are hard to read with a non-zero axis (unlike line graphs) -- I'll add an option to override this later.</message>
  <tree>f8eaee7b615dd2d578ef7d3d6e392c059c0271de</tree>
  <committer>
    <name>Alex Young</name>
    <email>alex@helicoid.net</email>
  </committer>
</commit>
