1. Lars Yencken
  2. color-stream-viz

Source

color-stream-viz / src / stream_viz.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#  stream_viz.py
#

"""
Generate JS data files for D3js stream vizualisations.
"""

import os
import optparse
from collections import defaultdict, Counter
import json
import colorsys
import sys
import csv

from colormath.color_objects import RGBColor
import colors

COLORSET = os.path.join(os.path.dirname(__file__), '..', 'inputs', 'colorset')

def color_ok(color):
    c = RGBColor()
    c.set_from_rgb_hex(color)
    h, s, v = colorsys.rgb_to_hsv(c.rgb_r / 255.0, c.rgb_g / 255.0, c.rgb_b
            / 255.0)
    return 0.25 < v < 0.90 and 0.1 < s

def hue(hex_color):
    c = RGBColor()
    c.set_from_rgb_hex(hex_color)
    h, s, v = colorsys.rgb_to_hsv(c.rgb_r / 255.0, c.rgb_g / 255.0, c.rgb_b
            / 255.0)
    return (h + 0.20) % 1

def hue_then_luminance(hex_color):
    c = RGBColor()
    c.set_from_rgb_hex(hex_color)
    h, s, v = colorsys.rgb_to_hsv(c.rgb_r / 255.0, c.rgb_g / 255.0, c.rgb_b
            / 255.0)
    return h*v

def luminance(hex_color):
    c = RGBColor()
    c.set_from_rgb_hex(hex_color)
    return colorsys.rgb_to_hsv(c.rgb_r / 255.0, c.rgb_g / 255.0,
            c.rgb_b / 255.0)[2]

class GlobalPopularity(Counter):
    "A callable distribution of colors over the entire period."
    def __init__(self, color_by_month):
        for colordist in color_by_month.itervalues():
            for color, count in colordist.items():
                self[color] += count

    def __call__(self, c):
        return self[c]

def generate_datafile(ordering='hue', normalize=False):
    lines = sys.stdin
    rows = csv.reader(lines, delimiter=' ')

    # map from month -> (color -> count)
    color_by_month = defaultdict(lambda: defaultdict(int))

    for month, color, count in rows:
        count = int(count)
        if color_ok(color):
            color_by_month[month][color] = count

    if normalize:
        for colordist in color_by_month.itervalues():
            total = float(sum(colordist.itervalues()))
            for c in colordist.keys():
                colordist[c] = 100.0 * (colordist[c] / total)

    # build the values for the stream diagram (color_points)
    color_to_name = colors.ReferencePalette.from_file(COLORSET)._names
    included_colors = sorted(filter(color_ok, color_to_name))
    color_points = {}
    months = sorted(color_by_month)
    for col in included_colors:
        points = [{'x': i, 'y': color_by_month[month][col]} for i, month in
                enumerate(months)]
        color_points[col] = points

    #def frequency(hex_color):
        #return sum(color_by_day[hex_color].values())

    if ordering == 'hue':
        key_method = hue
        ordered_colors = sorted(included_colors, key=key_method)
    elif ordering == 'popularity':
        key_method = GlobalPopularity(color_by_month)
        ordered_colors = sorted(included_colors, key=key_method, reverse=True)
        new_ordering = []
        for i in xrange(len(ordered_colors)):
            if i % 2 == 0:
                new_ordering.append(ordered_colors[i])
            else:
                new_ordering[0:0] = [ordered_colors[i]]
        ordered_colors = new_ordering
    else:
        raise ValueError('unknown ordering: %s' % ordering)

    # work out the order to stack them

    print 'var n = %d;' % len(ordered_colors)
    print 'var m = %d;' % len(months)
    print 'var color_points =', json.dumps(color_points) + ';'
    print 'var ordered_colors =', json.dumps(ordered_colors) + ';'

#----------------------------------------------------------------------------#

def _create_option_parser():
    usage = \
"""%prog [options]

Generates a JS file of data for D3js stream visualisations to stdout,
given a set of timestamped color counts on stdin."""

    parser = optparse.OptionParser(usage)
    parser.add_option('-o', '--ordering', action='store', dest='ordering',
            help='The ordering to use ([hue]/popularity)',
            default='hue')
    parser.add_option('-n', '--normalize', action='store_true',
            dest='normalize', default=False,
            help='Normalize counts within each time period')

    return parser

def main(argv):
    parser = _create_option_parser()
    (options, args) = parser.parse_args(argv)

    if args:
        parser.print_help()
        sys.exit(1)

    generate_datafile(ordering=options.ordering, normalize=options.normalize)

#----------------------------------------------------------------------------#

if __name__ == '__main__':
    main(sys.argv[1:])