diff options
Diffstat (limited to 'plot/style.py')
-rw-r--r-- | plot/style.py | 185 |
1 files changed, 161 insertions, 24 deletions
diff --git a/plot/style.py b/plot/style.py index 4e2057f..f7b3a35 100644 --- a/plot/style.py +++ b/plot/style.py | |||
@@ -1,22 +1,99 @@ | |||
1 | from common import log_once | ||
1 | from collections import namedtuple | 2 | from collections import namedtuple |
3 | from parse.tuple_table import TupleTable | ||
4 | |||
5 | import itertools | ||
2 | import matplotlib.pyplot as plot | 6 | import matplotlib.pyplot as plot |
3 | 7 | ||
4 | class Style(namedtuple('SS', ['marker', 'line', 'color'])): | 8 | class Style(namedtuple('SS', ['marker', 'color', 'line'])): |
5 | def fmt(self): | 9 | def fmt(self): |
6 | return self.marker + self.line + self.color | 10 | return self.marker + self.line + self.color |
7 | 11 | ||
12 | class ExcessVarietyException(Exception): | ||
13 | '''Too many fields or field values to use field style''' | ||
14 | pass | ||
15 | |||
16 | def make_styler(col_map): | ||
17 | try: | ||
18 | return FieldStyle(col_map.get_values()) | ||
19 | except ExcessVarietyException: | ||
20 | # Fallback, don't style by field values, instead create | ||
21 | # a unique style for every combination of field values possible | ||
22 | # This is significantly harder to visually parse | ||
23 | log_once("Too many columns and/or column values to create pretty " | ||
24 | "and simple graphs!\nGiving each combination of properties " | ||
25 | "its own line.") | ||
26 | return CombinationStyle(col_map) | ||
27 | |||
8 | class StyleMap(object): | 28 | class StyleMap(object): |
9 | '''Maps configs (dicts) to specific line styles.''' | 29 | # The base style, a solid black line |
30 | # The values of columns are used to change this line | ||
10 | DEFAULT = Style(marker='', line= '-', color='k') | 31 | DEFAULT = Style(marker='', line= '-', color='k') |
32 | |||
33 | def __init__(self, col_values): | ||
34 | raise NotImplementedError() | ||
35 | |||
36 | def _all_styles(self): | ||
37 | '''A dict holding all possible values for style each property.''' | ||
38 | return Style(marker=list('.,ov^<>1234sp*hH+xDd|_'), | ||
39 | line=['-', ':', '--'], | ||
40 | color=list('kbgrcmy'))._asdict() | ||
41 | |||
42 | def get_style(self, kv): | ||
43 | '''Translate column values to unique line style.''' | ||
44 | raise NotImplementedError() | ||
45 | |||
46 | def get_key(self): | ||
47 | '''A visual description of this StyleMap.''' | ||
48 | raise NotImplementedError() | ||
49 | |||
50 | |||
51 | class FieldStyle(StyleMap): | ||
52 | '''Changes properties of a line style by the values of each field.''' | ||
53 | |||
11 | ORDER = [ str, bool, float, int ] | 54 | ORDER = [ str, bool, float, int ] |
12 | 55 | ||
13 | def __init__(self, col_list, col_values): | 56 | def __init__(self, col_values): |
14 | '''Assign (some) columns in @col_list to fields in @Style to vary, and | 57 | '''Assign (some) columns in @col_list to fields in @Style to vary, and |
15 | assign values for these columns to specific field values.''' | 58 | assign values for these columns to specific field values.''' |
59 | # column->map(column_value->field_value) | ||
16 | self.value_map = {} | 60 | self.value_map = {} |
61 | # column->style_field | ||
17 | self.field_map = {} | 62 | self.field_map = {} |
18 | 63 | ||
19 | # Prioritize non-numbers | 64 | if len(col_values.keys()) > len(FieldStyle.DEFAULT): |
65 | raise ExcessVarietyException("Too many columns to style!") | ||
66 | |||
67 | col_list = self.__get_sorted_columns(col_values) | ||
68 | field_list = self.__get_sorted_fields() | ||
69 | field_dict = self._all_styles() | ||
70 | |||
71 | while len(col_list) < len(field_list): | ||
72 | curr_col = col_list[-1] | ||
73 | check_field = field_list[-2] | ||
74 | if len(col_values[curr_col]) <= len(field_dict[check_field]): | ||
75 | field_list.pop() | ||
76 | elif len(col_values[curr_col]) > len(field_dict[field_list[-1]]): | ||
77 | raise ExcessVarietyException("Too many values to style!") | ||
78 | else: | ||
79 | field_list.pop(0) | ||
80 | |||
81 | # Pair each column with a style field | ||
82 | for i in xrange(len(col_list)): | ||
83 | column = col_list[i] | ||
84 | field = field_list[i] | ||
85 | field_values = field_dict[field] | ||
86 | |||
87 | # Give each unique value of column a matching unique value of field | ||
88 | value_dict = {} | ||
89 | for value in sorted(col_values[column]): | ||
90 | value_dict[value] = field_values.pop(0) | ||
91 | |||
92 | self.value_map[column] = value_dict | ||
93 | self.field_map[column] = field | ||
94 | |||
95 | def __get_sorted_columns(self, col_values): | ||
96 | # Break ties using the type of the column | ||
20 | def type_priority(column): | 97 | def type_priority(column): |
21 | value = col_values[column].pop() | 98 | value = col_values[column].pop() |
22 | col_values[column].add(value) | 99 | col_values[column].add(value) |
@@ -25,30 +102,22 @@ class StyleMap(object): | |||
25 | except: | 102 | except: |
26 | t = bool if value in ['True','False'] else str | 103 | t = bool if value in ['True','False'] else str |
27 | return StyleMap.ORDER.index(t) | 104 | return StyleMap.ORDER.index(t) |
28 | col_list = sorted(col_list, key=type_priority) | ||
29 | 105 | ||
30 | # TODO: undo this, switch to popping mechanism | 106 | def column_compare(cola, colb): |
31 | for field, values in reversed([x for x in self.__get_all()._asdict().iteritems()]): | 107 | lena = len(col_values[cola]) |
32 | if not col_list: | 108 | lenb = len(col_values[colb]) |
33 | break | 109 | if lena == lenb: |
110 | return type_priority(cola) - type_priority(colb) | ||
111 | else: | ||
112 | return lena - lenb | ||
34 | 113 | ||
35 | next_column = col_list.pop(0) | 114 | return sorted(col_values.keys(), cmp=column_compare) |
36 | value_dict = {} | ||
37 | |||
38 | for value in sorted(col_values[next_column]): | ||
39 | value_dict[value] = values.pop(0) | ||
40 | |||
41 | self.value_map[next_column] = value_dict | ||
42 | self.field_map[next_column] = field | ||
43 | 115 | ||
44 | def __get_all(self): | 116 | def __get_sorted_fields(self): |
45 | '''A Style holding all possible values for each property.''' | 117 | fields = self._all_styles() |
46 | return Style(marker=list('.,ov^<>1234sp*hH+xDd|_'), | 118 | return sorted(fields.keys(), key=lambda x: len(fields[x])) |
47 | line=['-', ':', '--'], | ||
48 | color=list('bgrcmyk')) | ||
49 | 119 | ||
50 | def get_style(self, kv): | 120 | def get_style(self, kv): |
51 | '''Translate column values to unique line style.''' | ||
52 | style_fields = {} | 121 | style_fields = {} |
53 | 122 | ||
54 | for column, values in self.value_map.iteritems(): | 123 | for column, values in self.value_map.iteritems(): |
@@ -60,7 +129,6 @@ class StyleMap(object): | |||
60 | return StyleMap.DEFAULT._replace(**style_fields) | 129 | return StyleMap.DEFAULT._replace(**style_fields) |
61 | 130 | ||
62 | def get_key(self): | 131 | def get_key(self): |
63 | '''A visual description of this StyleMap.''' | ||
64 | key = [] | 132 | key = [] |
65 | 133 | ||
66 | for column, values in self.value_map.iteritems(): | 134 | for column, values in self.value_map.iteritems(): |
@@ -75,3 +143,72 @@ class StyleMap(object): | |||
75 | 143 | ||
76 | return sorted(key, key=lambda x:x[1]) | 144 | return sorted(key, key=lambda x:x[1]) |
77 | 145 | ||
146 | class CombinationStyle(StyleMap): | ||
147 | def __init__(self, col_map): | ||
148 | self.col_map = col_map | ||
149 | self.kv_styles = TupleTable(col_map) | ||
150 | self.kv_seen = TupleTable(col_map, lambda:False) | ||
151 | |||
152 | all_styles = self._all_styles() | ||
153 | styles_order = sorted(all_styles.keys(), | ||
154 | key=lambda x: len(all_styles[x]), | ||
155 | reverse = True) | ||
156 | |||
157 | # Add a 'None' option in case some lines are plotted without | ||
158 | # any value specified for this kv | ||
159 | column_values = col_map.get_values() | ||
160 | for key in column_values.keys(): | ||
161 | column_values[key].add(None) | ||
162 | |||
163 | styles_iter = self.__dict_combinations(all_styles, styles_order) | ||
164 | kv_iter = self.__dict_combinations(column_values) | ||
165 | |||
166 | # Cycle in case there are more kv combinations than styles | ||
167 | # This will be really, really ugly.. | ||
168 | styles_iter = itertools.cycle(styles_iter) | ||
169 | |||
170 | for kv, style in zip(kv_iter, styles_iter): | ||
171 | self.kv_styles[kv] = Style(**style) | ||
172 | |||
173 | for kv_tup, style in self.kv_styles: | ||
174 | kv = self.col_map.get_kv(kv_tup) | ||
175 | if not self.kv_styles[kv]: | ||
176 | raise Exception("Didn't initialize %s" % kv) | ||
177 | |||
178 | def __dict_combinations(self, list_dict, column_order = None): | ||
179 | def helper(set_columns, remaining_columns): | ||
180 | if not remaining_columns: | ||
181 | yield set_columns | ||
182 | return | ||
183 | |||
184 | next_column = remaining_columns.pop(0) | ||
185 | |||
186 | for v in list_dict[next_column]: | ||
187 | set_columns[next_column] = v | ||
188 | for vals in helper(dict(set_columns), list(remaining_columns)): | ||
189 | yield vals | ||
190 | |||
191 | if not column_order: | ||
192 | # Just use the random order returned by the dict | ||
193 | column_order = list_dict.keys() | ||
194 | |||
195 | return helper({}, column_order) | ||
196 | |||
197 | def get_style(self, kv): | ||
198 | self.kv_seen[kv] = True | ||
199 | return self.kv_styles[kv] | ||
200 | |||
201 | def get_key(self): | ||
202 | key = [] | ||
203 | |||
204 | for kv_tup, style in self.kv_styles: | ||
205 | kv = self.col_map.get_kv(kv_tup) | ||
206 | if not self.kv_seen[kv]: | ||
207 | continue | ||
208 | |||
209 | styled_line = plot.plot([], [], style.fmt())[0] | ||
210 | description = self.col_map.encode(kv, minimum=True) | ||
211 | |||
212 | key += [(styled_line, description)] | ||
213 | |||
214 | return sorted(key, key=lambda x:x[1]) | ||