Return an rgb color value interpolated from given color_stops;
    assumes colors in color_stops provided as strings of form 'rgb(RRR,GGG,BBB)'
    or in hex: '#RRGGBB'

def color_map(lookup, color_stops, default_color='rgb(122,122,122)'):
    """Return an rgb color value interpolated from given color_stops;
    assumes colors in color_stops provided as strings of form 'rgb(RRR,GGG,BBB)'
    or in hex: '#RRGGBB'
    """
    # if no color_stops, use default color
    if len(color_stops) == 0:
        return default_color
    
    # dictionary to lookup color from match-type color_stops
    match_map = dict((x, y) for (x, y) in color_stops)

    # if lookup matches stop exactly, return corresponding color (first priority)
    # (includes non-numeric color_stop "keys" for finding color by match)
    if lookup in match_map.keys():
        return match_map.get(lookup)

    # if lookup value numeric, map color by interpolating from color scale
    if isinstance(lookup, (int, float, complex)):

        # try ordering stops 
        try:
            stops, colors = zip(*sorted(color_stops))
        
        # if not all stops are numeric, attempt looking up as if categorical stops
        except TypeError:
            return match_map.get(lookup, default_color)

        # for interpolation, all stops must be numeric
        if not all(isinstance(x, (int, float, complex)) for x in stops):
            return default_color

        # check if lookup value in stops bounds
        if float(lookup) <= stops[0]:
            return colors[0]
        
        elif float(lookup) >= stops[-1]:
            return colors[-1]
        
        # check if lookup value matches any stop value
        elif float(lookup) in stops:
            return colors[stops.index(lookup)]
        
        # interpolation required
        else:

            rgb_tuples = [Color(rgb_tuple_from_str(x)) for x in colors]

            # identify bounding color stop values
            lower = max([stops[0]] + [x for x in stops if x < lookup])
            upper = min([stops[-1]] + [x for x in stops if x > lookup])
            
            # colors from bounding stops
            lower_color = rgb_tuples[stops.index(lower)]
            upper_color = rgb_tuples[stops.index(upper)]
            
            # generate color scale for mapping lookup value to interpolated color
            scale = Scale(Color(lower_color), Color(upper_color))

            # compute linear "relative distance" from lower bound color to upper bound color
            distance = (lookup - lower) / (upper - lower)

            # return string representing rgb color value
            return scale(distance).to_string().replace(', ', ',')

    # default color value catch-all
    return default_color