2
2
unicode_literals )
3
3
4
4
from matplotlib .patches import Rectangle
5
- import matplotlib .pyplot as plt
6
5
from matplotlib .offsetbox import AnchoredOffsetbox , TextArea , DrawingArea , HPacker , VPacker
6
+ from collections import defaultdict
7
7
import matplotlib .lines as mlines
8
- import operator
9
8
import numpy as np
10
9
11
- import six
12
10
13
11
"""
14
12
A legend is a dict of type
30
28
- value for discrete mappings.
31
29
"""
32
30
33
- def make_title (title ):
34
- title = title .title ()
35
- return TextArea (" %s " % title , textprops = dict (color = "k" , fontweight = "bold" ))
36
-
37
- def make_shape_key (label , shape ):
38
- idx = len (label )
39
- pad = 20 - idx
40
- lab = label [:max (idx , 20 )]
41
- pad = " " * pad
42
- label = TextArea (" %s" % lab , textprops = dict (color = "k" ))
43
- viz = DrawingArea (15 , 20 , 0 , 0 )
44
- fontsize = 10
45
- key = mlines .Line2D ([0.5 * fontsize ], [0.75 * fontsize ], marker = shape ,
46
- markersize = (0.5 * fontsize ), c = "k" )
47
- viz .add_artist (key )
48
- return HPacker (children = [viz , label ], align = "center" , pad = 5 , sep = 0 )
49
-
50
- def make_size_key (label , size ):
51
- if not isinstance (label , six .string_types ):
52
- label = round (label , 2 )
53
- label = str (label )
54
- idx = len (label )
55
- pad = 20 - idx
56
- lab = label [:max (idx , 20 )]
57
- pad = " " * pad
58
- label = TextArea (" %s" % lab , textprops = dict (color = "k" ))
59
- viz = DrawingArea (15 , 20 , 0 , 0 )
60
- fontsize = 10
61
- key = mlines .Line2D ([0.5 * fontsize ], [0.75 * fontsize ], marker = "o" ,
62
- markersize = size / 20. , c = "k" )
63
- viz .add_artist (key )
64
- return HPacker (children = [viz , label ], align = "center" , pad = 5 , sep = 0 )
65
-
66
- # TODO: Modify to correctly handle both, color and fill
67
- # to include an alpha
68
- def make_line_key (label , color ):
69
- label = str (label )
70
- idx = len (label )
71
- pad = 20 - idx
72
- lab = label [:max (idx , 20 )]
73
- pad = " " * pad
74
- label = TextArea (" %s" % lab , textprops = dict (color = "k" ))
75
- viz = DrawingArea (20 , 20 , 0 , 0 )
76
- viz .add_artist (Rectangle ((0 , 5 ), width = 16 , height = 5 , fc = color ))
77
- return HPacker (children = [viz , label ], height = 25 , align = "center" , pad = 5 , sep = 0 )
78
-
79
- def make_linetype_key (label , linetype ):
80
- idx = len (label )
81
- pad = 20 - idx
82
- lab = label [:max (idx , 20 )]
83
- pad = " " * pad
84
- label = TextArea (" %s" % lab , textprops = dict (color = "k" ))
85
- viz = DrawingArea (30 , 20 , 0 , 0 )
86
- fontsize = 10
87
- x = np .arange (0.5 , 2.25 , 0.25 ) * fontsize
88
- y = np .repeat (0.75 , 7 ) * fontsize
89
-
90
- key = mlines .Line2D (x , y , linestyle = linetype , c = "k" )
91
- viz .add_artist (key )
92
- return HPacker (children = [viz , label ], align = "center" , pad = 5 , sep = 0 )
93
-
94
- legend_viz = {
95
- "color" : make_line_key ,
96
- "fill" : make_line_key ,
97
- "linetype" : make_linetype_key ,
98
- "shape" : make_shape_key ,
99
- "size" : make_size_key ,
100
- }
101
-
102
31
def add_legend (legend , ax ):
103
32
"""
104
33
Add a legend to the axes
@@ -109,50 +38,144 @@ def add_legend(legend, ax):
109
38
Specification in components.legend.py
110
39
ax: axes
111
40
"""
112
- # TODO: Implement alpha
113
- # It should be coupled with fill, if fill is not
114
- # part of the aesthetics, then with color
115
- remove_alpha = 'alpha' in legend
116
- if remove_alpha :
117
- _alpha_entry = legend .pop ('alpha' )
118
-
41
+ # Group legends by column name and invert color/label mapping
42
+ groups = {}
43
+ for aesthetic in legend :
44
+ legend_entry = legend [aesthetic ]
45
+ column_name = legend_entry ["column_name" ]
46
+ g = groups .get (column_name , {})
47
+ legend_dict = { l :c for c ,l in legend_entry ['dict' ].items () }
48
+ g [aesthetic ] = defaultdict (lambda : None , legend_dict )
49
+ groups [column_name ] = g
50
+
51
+ nb_rows = 0
52
+ nb_cols = 0
53
+ max_rows = 18
119
54
# py3 and py2 have different sorting order in dics,
120
55
# so make that consistent
121
- for i , aesthetic in enumerate (sorted (legend .keys ())):
122
- legend_entry = legend [aesthetic ]
123
- new_legend = draw_entry (ax , legend_entry , aesthetic , i )
124
- ax .add_artist (new_legend )
125
-
126
- if remove_alpha :
127
- legend ['alpha' ] = _alpha_entry
128
-
129
- def draw_entry (ax , legend_entry , aesthetic , ith_entry ):
130
- children = []
131
- children .append (make_title (legend_entry ['column_name' ]))
132
- viz_handler = legend_viz [aesthetic ]
133
- legend_items = sorted (legend_entry ['dict' ].items (), key = operator .itemgetter (1 ))
134
- children += [viz_handler (str (lab ), col ) for col , lab in legend_items ]
135
- box = VPacker (children = children , align = "left" , pad = 0 , sep = 5 )
136
-
137
- # TODO: The vertical spacing between the legends isn't consistent. Should be
138
- # padded consistently
139
- anchored_box = AnchoredOffsetbox (loc = 6 ,
140
- child = box , pad = 0. ,
141
- frameon = False ,
142
- #bbox_to_anchor=(0., 1.02),
143
- # Spacing goes here
144
- bbox_to_anchor = (1 , 0.8 - 0.35 * ith_entry ),
145
- bbox_transform = ax .transAxes ,
146
- borderpad = 1. ,
147
- )
56
+ for i , column_name in enumerate (sorted (groups .keys ())):
57
+ legend_group = groups [column_name ]
58
+ legend_box , rows = draw_legend_group (legend_group , column_name , i )
59
+ cur_nb_rows = nb_rows
60
+ nb_rows += rows + 1
61
+ if nb_rows > max (max_rows , rows + 1 ) :
62
+ nb_cols += 1
63
+ nb_rows = 0
64
+ cur_nb_rows = 0
65
+ anchor_legend (ax , legend_box , cur_nb_rows , nb_cols )
66
+
67
+
68
+ def draw_legend_group (legends , column_name , ith_group ):
69
+ labels = get_labels (legends )
70
+ colors , has_color = get_colors (legends )
71
+ legend_title = make_title (column_name )
72
+ legend_labels = [make_label (l ) for l in labels ]
73
+ none_dict = defaultdict (lambda : None )
74
+ legend_cols = []
75
+
76
+ # TODO: Implement alpha
77
+ if "shape" in legends or "size" in legends :
78
+ shapes = legends ["shape" ] if "shape" in legends else none_dict
79
+ #sizes = map(int,labels) if "size" in legends else none_dict
80
+ sizes = legends ["size" ] if "size" in legends else none_dict
81
+ line = lambda l : make_shape (colors [l ], shapes [l ], sizes [l ])
82
+ legend_shapes = [line (label ) for label in labels ]
83
+ legend_cols .append (legend_shapes )
84
+ if "linetype" in legends :
85
+ linetypes = legends ["linetype" ]
86
+ legend_lines = [make_line (colors [l ], linetypes [l ]) for l in labels ]
87
+ legend_cols .append (legend_lines )
88
+ # If we don't have lines, indicate color with a rectangle
89
+ if "linetype" not in legends and has_color :
90
+ legend_rects = [make_rect (colors [l ]) for l in labels ]
91
+ legend_cols .append (legend_rects )
92
+ # Concatenate columns and compile rows
93
+ legend_cols .append (legend_labels )
94
+ row = lambda l : HPacker (children = l , height = 25 , align = 'center' , pad = 5 , sep = 0 )
95
+ legend_rows = [row (legend_items ) for legend_items in zip (* legend_cols )]
96
+ # Vertically align items and anchor in plot
97
+ rows = [legend_title ] + legend_rows
98
+ box = VPacker (children = rows , align = "left" , pad = 0 , sep = - 10 )
99
+ return box , len (rows )
100
+
101
+
102
+
103
+ def anchor_legend (ax , box , row , col ):
104
+ anchored = AnchoredOffsetbox (loc = 2 ,
105
+ child = box ,
106
+ pad = 0. ,
107
+ frameon = False ,
108
+ #bbox_to_anchor=(0., 1.02),
109
+ # Spacing goes here
110
+ #bbox_to_anchor=(1, 0.8 - 0.35 * ith_group),
111
+ #bbox_to_anchor=(1.01 + 0.1*col, 0.86 - 0.06*row),
112
+ bbox_to_anchor = (1 + 0.25 * col , 1 - 0.054 * row ),
113
+ bbox_transform = ax .transAxes ,
114
+ #borderpad=1.,
115
+ )
148
116
# Workaround for a bug in matplotlib up to 1.3.1
149
117
# https://github.com/matplotlib/matplotlib/issues/2530
150
- anchored_box .set_clip_on (False )
151
- return anchored_box
118
+ anchored .set_clip_on (False )
119
+ ax .add_artist (anchored )
120
+
121
+
122
+
123
+ def make_title (title ):
124
+ title = title .title ()
125
+ area = TextArea (" %s " % title , textprops = dict (color = "k" , fontweight = "bold" ))
126
+ viz = DrawingArea (20 , 10 , 0 , 0 )
127
+ packed = VPacker (children = [area , viz ], align = "center" , pad = 0 , sep = 0 )
128
+ return packed
129
+
130
+
131
+
132
+ def make_shape (color , shape , size , y_offset = 10 , height = 20 ):
133
+ color = color if color != None else "#222222" # Default value if None
134
+ shape = shape if shape != None else "o"
135
+ size = size if size != None else 75
136
+ viz = DrawingArea (30 , height , 8 , 1 )
137
+ key = mlines .Line2D ([0 ], [y_offset ], marker = shape ,
138
+ markersize = size / 12.0 , c = color )
139
+ viz .add_artist (key )
140
+ return viz
141
+
142
+
143
+
144
+ def make_line (color , style , width = 20 , y_offset = 10 , height = 20 , linewidth = 3 ):
145
+ color = color if color != None else "k" # Default value if None
146
+ style = style if style != None else "-"
147
+ viz = DrawingArea (30 , 10 , 0 , - 5 )
148
+ x = np .arange (0.0 , width , width / 7.0 )
149
+ y = np .repeat (y_offset , x .size )
150
+ key = mlines .Line2D (x , y , linestyle = style , linewidth = linewidth , c = color )
151
+ viz .add_artist (key )
152
+ return viz
153
+
154
+
155
+
156
+ def make_rect (color , size = (20 ,6 ), height = 20 ):
157
+ color = color if color != None else "k" # Default value if None
158
+ viz = DrawingArea (30 , height , 0 , 1 )
159
+ viz .add_artist (Rectangle ((0 , 6 ), width = size [0 ], height = size [1 ], fc = color ))
160
+ return viz
161
+
162
+
163
+
164
+ def make_label (label , max_length = 20 , capitalize = True ):
165
+ label_text = str (label ).title ()[:max_length ]
166
+ label_area = TextArea (label_text , textprops = dict (color = "k" ))
167
+ return label_area
168
+
169
+
170
+ def get_labels (legends ) :
171
+ # All the legends are for the same column, so the labels of any will do
172
+ return sorted (legends .values ()[0 ].keys ())
152
173
153
- if __name__ == "__main__" :
154
- fig = plt .figure ()
155
- ax = fig .add_axes ([0.1 , 0.1 , 0.4 , 0.7 ])
156
174
157
- ax .add_artist (draw_legend (ax ,{1 : "blah" , 2 : "blah2" , 15 : "blah4" }, "size" , 1 ))
158
- plt .show (block = True )
175
+ def get_colors (legends ) :
176
+ if "color" in legends :
177
+ return legends ["color" ], True
178
+ elif "fill" in legends :
179
+ return legends ["fill" ], True
180
+ else :
181
+ return defaultdict (lambda : None ), False
0 commit comments