Never been to DZone Snippets before?

Snippets is a public source code repository. Easily build up your personal collection of code snippets, categorize them with tags / keywords, and share them with the world

About this user

Todd Sayre http://del.icio.us/sporkyy

« Newer Snippets
Older Snippets »
Showing 1-10 of 13 total  RSS 

Rails helpers for A List Apart No. 256

I cooked up some Ruby on Rails helpers for testing the techniques in the article Accessible Data Visualization with Web Standards from A List Apart No. 256. If you want to know more about Rails, check out No. 257. If you want to be smarter, read A List Apart every week.

def chartlist(data)
  total = data.inject(0.0) { |sum, datum| sum + datum[:count] }
  bars = ''

  data.each do |datum|
    link  = content_tag 'a', datum[:name], :href => datum[:href]
    count = content_tag 'span', datum[:count], :class => 'count'
    index = content_tag 'span', "(#{(datum[:count]/total*100).to_i}%)", :class => 'index', :style => "width: #{(datum[:count]/total*100).to_i}%"
    bars << content_tag('li', link << count << index)
  end

  content_tag 'ul', bars, :class => 'chartlist'
end


Sample data for a chartlist (example)

data = [{ :name => 'Apples',   :count => 420, :href => 'http://www.example.com/fruits/apples/' },
        { :name => 'Bananas',  :count => 280, :href => 'http://www.example.com/fruits/bananas/' },
        { :name => 'Cherries', :count => 200, :href => 'http://www.example.com/fruits/cherries/' },
        { :name => 'Dates',    :count => 100, :href => 'http://www.example.com/fruits/dates/' }]


def sparkline(data)
  max = data.sort.last.to_f
  sparklines = ''

  data.each_with_index do |datum, index|
    count_string = datum.to_s
    '(' << count_string if index == 0
    count_string << ',' if index != data.length
    count_string << ')' if index == data.length
    count = content_tag 'span', count_string, :class => 'count', :style => "height: #{(datum/max*100).to_i}%"
    index = content_tag 'span', count << ' ', :class => 'index'
    sparklines << index
  end

  content_tag('span', sparklines, :class => 'sparkline')
end


Sample data for sparklines (example)

data = [60, 220, 140, 80, 110, 90, 180, 140, 120, 160, 175, 225, 175, 125]


def timeline(data)
  max = data.sort { |a, b| a[:count] <=> b[:count] }.last[:count]
  bars = ''

  data.each do |datum|
    label = content_tag 'span', datum[:label], :class => 'label'
    count = content_tag 'span', "(#{datum[:count]})", :class => 'count', :style => "height: #{(datum[:count]/max.to_f*100).to_i}%"
    link  = content_tag 'a', label << count, :href => datum[:href], :title => "#{datum[:label]}: #{datum[:count]}"
    bars << content_tag('li', link, :style => "width: #{(100.0/data.length).to_i}%")
  end

  content_tag 'ul', bars, :class => 'timeline'
end


Sample data for a timeline (example)

data = [{ :date => '2007-12-01', :count =>  40, label => '1' },
        { :date => '2007-12-02', :count => 100, label => '2' },
        { :date => '2007-12-03', :count => 150, label => '3' }]

Blueprint CSS forms extension

Changes the widths of form elements so they fit into smaller columns created using the Blueprint CSS framework.

div.span-1 input.text, div.span-1 input.title { width:  30px; }
div.span-2 input.text, div.span-2 input.title { width:  50px; }
div.span-3 input.text, div.span-3 input.title { width:  90px; }
div.span-4 input.text, div.span-4 input.title { width: 130px; }
div.span-5 input.text, div.span-5 input.title { width: 170px; }
div.span-6 input.text, div.span-6 input.title { width: 210px; }
div.span-7 input.text, div.span-7 input.title { width: 250px; }
div.span-8 input.text, div.span-8 input.title { width: 290px; }

div.span-1 select { width:  30px; }
div.span-2 select { width:  50px; }
div.span-3 select { width:  90px; }
div.span-4 select { width: 130px; }
div.span-5 select { width: 170px; }

div.span-1  textarea { width:  30px; height:  25px; }
div.span-2  textarea { width:  50px; height:  50px; }
div.span-3  textarea { width:  90px; height:  75px; }
div.span-4  textarea { width: 130px; height: 100px; }
div.span-5  textarea { width: 170px; height: 125px; }
div.span-6  textarea { width: 210px; height: 150px; }
div.span-7  textarea { width: 250px; height: 175px; }
div.span-8  textarea { width: 290px; height: 200px; }
div.span-9  textarea { width: 330px; height: 225px; }
div.span-10 textarea { width: 370px; height: 250px; }

Collapse ranges in arrays

Takes an Array and turns consecutive integers into Range objects.

class Array
    def collapse_ranges
        return self if self.length <= 2
        self.uniq!
        self.sort! rescue nil
        temp_array, return_array = [], []
        self.each_with_index do |item, i|
            if item.respond_to?(:next)
                temp_array.push item
                if item.next != self[i + 1]
                    return_array.concat 3 <= temp_array.length ?
                        [temp_array.first..temp_array.last] :
                         temp_array
                    temp_array.clear
                end
            else
                return_array.concat 3 <= temp_array.length ?
                    [temp_array.first..temp_array.last] :
                     temp_array
                temp_array.clear
                return_array.push item
            end
        end
        return return_array
    end
end


>> [1, 3, 4, 5, 7, 8].collapse_ranges.join(', ')
=> 1, 3..5, 7, 8
>> %w(a c d e g i j).collapse_ranges.join(', ')
=> a, c..e, g, i, j
>> [1, 2.5, 3, 4, 5].collapse_ranges.join(', ')
=> 1, 2.5, 3..5
>> [1, 2, 3, 4, {:test => 'value'}, 5].collapse_ranges.join(', ')
=> 1..4, testvalue, 5

Find missing array items

This works fine for integers and letters. It should work for anything though, so long as Ruby can form an range from the first and last array items.

class Array
    def missing_items
        return [] if self.length <= 1
        self.uniq!
        self.sort! rescue nil
        begin
            (self.first..self.last).to_a - self
        rescue
            []
        end
    end
end


>> [1, 3, 4, 10].missing_items.join(', ')
=> 2, 5, 6, 7, 8, 9
>> [1, 2, 7, 7.5, 8.2].missing_items.join(', ')
=> 3, 4, 5, 6, 8
>> %w(a b c f g j).missing_items.join(', ')
=> d, e, h, i
>> [2.5, {:test => 'value'}].missing_items.join(', ')
=>

Javascript Rails-like inflector

I was using Ajax to do some DOM manipulation in a Rails app I was working on. None of what I was doing needed to involve a round trip to server, the only reason I hit the server at all was to take advantage of Rails' (well, ActiveSupport's) pluralize function. Also, this technique required an extra action that I decided to eliminate as I try to move to a more RESTful architecture.

The only part of ActiveSupport's Inflector class I implemented are the ones I needed. I implemented even fewer of Rails' Inflector-based functions.

Stick all this in a file, such as "inflector.js".
/*
 * This script depends on no outside libraries.
 */

Inflector = {
    /*
     * The order of all these lists has been reversed from the way 
     * ActiveSupport had them to keep the correct priority.
     */
    Inflections: {
        plural: [
            [/(quiz)$/i,               "$1zes"  ],
            [/^(ox)$/i,                "$1en"   ],
            [/([m|l])ouse$/i,          "$1ice"  ],
            [/(matr|vert|ind)ix|ex$/i, "$1ices" ],
            [/(x|ch|ss|sh)$/i,         "$1es"   ],
            [/([^aeiouy]|qu)y$/i,      "$1ies"  ],
            [/(hive)$/i,               "$1s"    ],
            [/(?:([^f])fe|([lr])f)$/i, "$1$2ves"],
            [/sis$/i,                  "ses"    ],
            [/([ti])um$/i,             "$1a"    ],
            [/(buffal|tomat)o$/i,      "$1oes"  ],
            [/(bu)s$/i,                "$1ses"  ],
            [/(alias|status)$/i,       "$1es"   ],
            [/(octop|vir)us$/i,        "$1i"    ],
            [/(ax|test)is$/i,          "$1es"   ],
            [/s$/i,                    "s"      ],
            [/$/,                      "s"      ]
        ],
        singular: [
            [/(quiz)zes$/i,                                                    "$1"     ],
            [/(matr)ices$/i,                                                   "$1ix"   ],
            [/(vert|ind)ices$/i,                                               "$1ex"   ],
            [/^(ox)en/i,                                                       "$1"     ],
            [/(alias|status)es$/i,                                             "$1"     ],
            [/(octop|vir)i$/i,                                                 "$1us"   ],
            [/(cris|ax|test)es$/i,                                             "$1is"   ],
            [/(shoe)s$/i,                                                      "$1"     ],
            [/(o)es$/i,                                                        "$1"     ],
            [/(bus)es$/i,                                                      "$1"     ],
            [/([m|l])ice$/i,                                                   "$1ouse" ],
            [/(x|ch|ss|sh)es$/i,                                               "$1"     ],
            [/(m)ovies$/i,                                                     "$1ovie" ],
            [/(s)eries$/i,                                                     "$1eries"],
            [/([^aeiouy]|qu)ies$/i,                                            "$1y"    ],
            [/([lr])ves$/i,                                                    "$1f"    ],
            [/(tive)s$/i,                                                      "$1"     ],
            [/(hive)s$/i,                                                      "$1"     ],
            [/([^f])ves$/i,                                                    "$1fe"   ],
            [/(^analy)ses$/i,                                                  "$1sis"  ],
            [/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i, "$1$2sis"],
            [/([ti])a$/i,                                                      "$1um"   ],
            [/(n)ews$/i,                                                       "$1ews"  ],
            [/s$/i,                                                            ""       ]
        ],
        irregular: [
            ['move',   'moves'   ],
            ['sex',    'sexes'   ],
            ['child',  'children'],
            ['man',    'men'     ],
            ['person', 'people'  ]
        ],
        uncountable: [
            "sheep",
            "fish",
            "series",
            "species",
            "money",
            "rice",
            "information",
            "equipment"
        ]
    },
    ordinalize: function(number) {
        if (11 <= parseInt(number) % 100 && parseInt(number) % 100 <= 13) {
            return number + "th";
        } else {
            switch (parseInt(number) % 10) {
                case  1: return number + "st";
                case  2: return number + "nd";
                case  3: return number + "rd";
                default: return number + "th";
            }
        }
    },
    pluralize: function(word) {
        for (var i = 0; i < Inflector.Inflections.uncountable.length; i++) {
            var uncountable = Inflector.Inflections.uncountable[i];
            if (word.toLowerCase == uncountable) {
                return uncountable;
            }
        }
        for (var i = 0; i < Inflector.Inflections.irregular.length; i++) {
            var singular = Inflector.Inflections.irregular[i][0];
            var plural   = Inflector.Inflections.irregular[i][1];
            if ((word.toLowerCase == singular) || (word == plural)) {
                return plural;
            }
        }
        for (var i = 0; i < Inflector.Inflections.plural.length; i++) {
            var regex          = Inflector.Inflections.plural[i][0];
            var replace_string = Inflector.Inflections.plural[i][1];
            if (regex.test(word)) {
                return word.replace(regex, replace_string);
            }
        }
    }
}

function ordinalize(number) {
    return Inflector.ordinalize(number);
}

/*
 * Javascript doesn't have optional parameters or overloading so I had to use
 * the ever popular pseudo options hash object technique.
 * required properties:
 *     count    Number of objects to pluralize
 *     singular Singular noun for the objects
 * optional property:
 *     plural   Plural (probably irregular) noun for the objects
 * examples:
 *      pluralize({ count: total_count, singular: "Issue" })
 *      pluralize({ count: total_count, singular: "Goose", plural: "Geese" })
 */
function pluralize(options) {
    return options.count + " " + (1 == parseInt(options.count) ?
        options.singular :
        options.plural || Inflector.pluralize(options.singular));
}

Array percentages of the maximum

Returns an array whose elements are the percentage of each element compared to the maximum element in the array.

>> [25, 150, 200, 5, 75, 125].percentages_of_maximum
=> [12, 75, 100, 2, 37, 62]


If you're using Rails, stick this in your "environment.rb" script in the "config" directory:

class Array
    def percentages_of_maximum
        self.collect{ |i| ((i.to_f / self.sort.last.to_f) * 100).to_i }
    end
end

FirstLast (real classes for Ajaxability)

I know CSS has the :first-child and :last-child pseudo classes. But :first-child is a part of CSS 2 and has poor browsers support and :last-child is a part of CSS 3 and that's not worth thinking about using yet. The idea is sound though, so I worked up this JavaScript method of getting the same effect.

One advantage of doing this in JavaScript rather than using CSS is that the classes will change if you reorder the child nodes with more JavaScript. In my case I'm using Scriptaculous sortables. Or to be more specific, I'm using a Ruby on Rails helper method to make something sortable through Scriptaculous:
<%= sortable_element 'image-list',
    :constraint => false,
    :url => { :action => 'sort', :issue_id => params[:issue_id] }
-%>


I've chosen to not make this script run unobtrusively. Most of it can go into a file and be included in the header or added to your own common JavaScript include file. To get it to run on a page you will need to include something like the below in each page.
<script type="text/javascript">
//<![CDATA[
    Event.onDOMReady(function(){FirstLast.go("image-list")});
    Ajax.Responders.register({
        onComplete: function(){FirstLast.go("image-list");}
   });
//]]>
</script>

I use the DOMReady extension for Prototype's Event object, but you can use any loader you want. The Ajax.Responders.register part reruns the script after a drag-and-drop operation (or any Ajax operation really).

Download Scriptaculous and Prototype.

var FirstLast = {
    go: function(el) {
        el = $(el);

        // Whitespace nodes need to be cleaned to get the intended effect
        var children = Element.cleanWhitespace(el).childNodes;

        // Return if there are not any children
        if (0 == children.length) return
        
        if (1 == children.length) {
            // Cheap shortcut if there is only 1 child node
            children[0].addClassName(this._firstChildClassName);
            children[0].addClassName(this._lastChildClassName);
        } else {
            for (var i = 0; i < children.length; i++) {
                switch (i) {
                    // First child
                    case 0:
                        children[i].addClassName(this._firstChildClassName);
                        children[i].removeClassName(this._lastChildClassName);
                        break;
                    // Last child
                    case children.length - 1:
                        children[i].removeClassName(this._firstChildClassName);
                        children[i].addClassName(this._lastChildClassName);
                        break;
                    // Every child other than the first or last
                    default:
                        children[i].removeClassName(this._firstChildClassName);
                        children[i].removeClassName(this._lastChildClassName);
                        break;  // I know it is unnecessary
                }
            }
        }
    },
    // Pseudo Private methods and attributes
    _firstChildClassName: "first",
    _lastChildClassName: "last"
};

Number to Currency with Cents

A slight alteration to the default Rails currency formatting helper to show numbers in cents if the number is less than $1.00. For example $0.99 would instead become 99&cent;.

def number_to_currency_with_cents(number, options = {})
    options = options.stringify_keys
    precision = options.delete('precision') { 2 }
    unit = options.delete('unit') { '$' }
    fractional_unit = options.delete('fractional_unit') { '&cent;' }
    separator = options.delete('separator') { '.' }
    delimiter = options.delete('delimiter') { ',' }
    separator = '' unless precision > 0
    begin
        fraction = number.abs % 1.0
        body = number.floor
        if body != 0 || body == 0 && fraction == 0 then
            parts = number_with_precision(number, precision).split('.')
            unit + number_with_delimiter(parts[0], delimiter) + separator + parts[1].to_s
        else
            (fraction * 100).to_i.to_s + fractional_unit
        end
    rescue
        number
    end
end


I'm really tempted to go through and replace that whole thing with my own code, but it works, so I'm happy.

Gmail Date Format Helper

I needed a short and intuitive way of showing dates, so rather than just making something up I decided to steal Google's short date format from Gmail. I'm sure they did usability studies and whatnot.

ActiveSupport::CoreExtensions::Time::Conversions::DATE_FORMATS.merge!(
  :gmail => lambda { |date|
    Time.now.beginning_of_day <= date ?
    "#{date.strftime('%I').to_i}:#{date.strftime('%M')} #{date.strftime('%p').downcase}" :
    Time.now.beginning_of_year <= date ?
    "#{date.strftime('%b')} #{date.day}" :
    "#{date.month}/#{date.day}/#{date.strftime('%y')}"
  }
)

ActiveSupport::CoreExtensions::Date::Conversions::DATE_FORMATS.merge!(
  :gmail => lambda { |date|
    Time.now.beginning_of_day <= date ?
    "#{date.strftime('%I').to_i}:#{date.strftime('%M')} #{date.strftime('%p').downcase}" :
    Time.now.beginning_of_year <= date ?
    "#{date.strftime('%b')} #{date.day}" :
    "#{date.month}/#{date.day}/#{date.strftime('%y')}"
  }
)


Put this code in your "environmen.rb" file in your "RAILS_ROOT/config" directory or make a new Ruby script file containing it in your "RAILS_ROOT/config/initializers" directory.

ZebraStripes

/*
 * This script requires:
 *   1. Prototype version 1.5 or greater
 *     - Homepage: http://prototype.conio.net/
 *     - Download: http://script.aculo.us/downloads
 *   2. DomReady addon for Prototype
 *     - Homepage: http://www.vivabit.com/bollocks/2006/06/21/a-dom-ready-extension-for-prototype
 *     - Download: http://www.vivabit.com/code/domready/domready.js
 */

/*
 * I needed something capable of not just unobtrusively running when a page
 * was loaded but also easily being called at my whim as a part of an Ajax
 * transaction.
 */
var ZebraStripes = {
    /*
     * Call this function when you want something striped.  It will figure out
     * how to stripe the element.
     */
    stripe: function(el) {
        el = $(el);
        switch (el.tagName) {
            case "TABLE":
                this._stripeTable(el);
                break;
            case "OL":
            case "UL":
                this._stripeNormalList(el);
                break;
            case "DL":
                this._stripeDefinitionList(el);
                break;
        }
    },
    /***************************************************************************
     * Everything below here is psuedo-private
     **************************************************************************/
    /*
     * This class name will be applied to the odd numbered elements
     */
    _altClassName: "alt",
    /*
     * This property persists the data that tells the object whether
     * to stripe (_isEven == false) or unstripe (_isEven == true) an element
     */
    _isEven: true,
    /*
     * Cycles the _isEven property of this object between true and false
     */
    _cycle: function() {
        this._isEven = ! this._isEven;
    },
    /*
     * As a part of the Ajax-friendliness, it is important that we remove the
     * alt class from elements as well as add it.
     */
    _stripeElement: function(el) {
        el = $(el);
        if (this._isEven) {
            el.removeClassName(this._altClassName);
        } else {
            el.addClassName(this._altClassName);
        }
    },
    /*
     * This works to stripe the child nodes of TABLE, TBODY, OL and UL elements.
     */
    _stripeElements: function(els) {
        els = $(els);
        if (els.length == 0) {
            return
        }
        var parent = els[0].parentNode;
        this._isEven = true;
        for (var i = 0; i < els.length; i++ ) {
            if ((parent == els[i].parentNode) && (els[i].visible)) {
                this._stripeElement(els[i]);
                this._cycle();
            }
        }
    },
    /*
     * TBODY is not necessary, but I recommend it.  I debated about striping
     * THEAD and TFOOT, but chose not to.  Might be added later.
     */
    _stripeTable: function(table) {
        table = $(table);
        if (table.getElementsByTagName("tbody")) {
            var tableBodies = table.getElementsByTagName("tbody");
            for (var i = 0; i < tableBodies.length; i++) {
                this._stripeElements(tableBodies[i].getElementsByTagName("tr"));
            }
        } else {
            this._stripeElements(table.getElementsByTagName("tr"));
        }
    },
    /*
     * This stripes OL and UL since they both have LI and only LI child nodes.
     */
    _stripeNormalList: function(list) {
        list = $(list);
        this._stripeElements(list.getElementsByTagName("li"));
    },
    /*
     * I have seen other function out there that can stripe DL, but they all
     * assumed that each DT would have only one DD following it.  That is not
     * always the case, and not the case in the project that spawned this
     * javascript.
     */
    _stripeDefinitionList: function(list) {
        list = $(list);
        Element.cleanWhitespace(list);
        var children = list.childNodes;
        var previousDt;
        for (var i = 0; i < children.length; i++) {
            switch (children[i].tagName) {
                case "DT":
                    if (children[i].visible) {
                        this._stripeElement(children[i]);
                        this._cycle();
                    }
                    previousDt = children[i];
                    break;
                case "DD":
                    if (previousDt.visible) {
                        this._stripeElement(children[i]);
                    }
                    break;
            }
        }
    }
};

/*
 * This function will go ahead and stripe all the elligible elements on the
 * page when the page first loads.
 */
function initZebraStripes() {
    var toStripe = $$("dl.striped")
        .concat($$("ol.striped"))
        .concat($$("table.striped"))
        .concat($$("ul.striped"));

    toStripe.each(
        function(el) {
            ZebraStripes.stripe(el);
        }
    );
}

Event.onDOMReady(initZebraStripes);
// Or you could substitute a different loader:
//Event.observe(window, 'load', initZebraStripes)
« Newer Snippets
Older Snippets »
Showing 1-10 of 13 total  RSS