1 | """ Module dedicated to create treemap """ |
---|
2 | |
---|
3 | import colorsys |
---|
4 | from StringIO import StringIO |
---|
5 | |
---|
6 | from PIL import Image |
---|
7 | from PIL import ImageDraw |
---|
8 | from PIL import ImageFont |
---|
9 | |
---|
10 | from Products.Five.browser import BrowserView |
---|
11 | from views import SizeByPath |
---|
12 | from const import FONT_PATH, IMAGE_MASK, USER_FONT, WIDTH, HEIGHT, \ |
---|
13 | TEXT_SATURATION, TEXT_LIGHT, MAX_PERCENT, MAX_ANGLE, \ |
---|
14 | DIV_RGB, DIV_SIDE, DIVERSION_FATHER, DIVERSION_CHILD, \ |
---|
15 | DIVERSION_TEXT_CHILD_V, DIVERSION_TEXT_CHILD_H, \ |
---|
16 | FONT_SIZE_FATHER, FONT_SIZE_CHILDREN_MIN, FRAME_COLOR, \ |
---|
17 | USER_COLOR, COLOR_FIELD, DIVERSION_TEXT_BORDER, \ |
---|
18 | DIVERSION_CHILD, DIVERSION_VERTICAL, FRAME_DIVERSION |
---|
19 | |
---|
20 | class TreemapBTree: |
---|
21 | """ Class dedicated to create treemap binary tree """ |
---|
22 | def __init__(self, left=None, right=None, data=None): |
---|
23 | self.left = left |
---|
24 | self.right = right |
---|
25 | self.data = data |
---|
26 | |
---|
27 | def getWeight(self, treemap): |
---|
28 | """ Method gets size of treemap objects """ |
---|
29 | return float(sum(x.size for x in treemap)) |
---|
30 | |
---|
31 | def getHalfSize(self, items): |
---|
32 | """ Method gets index of treemap objects (half size) """ |
---|
33 | half_size = self.getWeight(items) / 2 |
---|
34 | pre_size = next_size = 0.0 |
---|
35 | for i,treemap in enumerate(items): |
---|
36 | next_size = pre_size + treemap.size |
---|
37 | if abs(half_size - next_size) > abs(half_size - pre_size): |
---|
38 | return i |
---|
39 | pre_size = next_size |
---|
40 | |
---|
41 | |
---|
42 | def addNodes(self, treemap_data): |
---|
43 | """ Recursive method adds treemap objects into TreemapBTree """ |
---|
44 | def leaf(self,treemap_data): |
---|
45 | """ Method sets leaf into treemap tree """ |
---|
46 | if len(treemap_data) == 0: |
---|
47 | return True |
---|
48 | |
---|
49 | elif isinstance(treemap_data, Treemap) and treemap_data.title == 'Root': |
---|
50 | self.addNodes(treemap_data[0]) |
---|
51 | return False |
---|
52 | |
---|
53 | elif len(treemap_data) == 1: |
---|
54 | if len(treemap_data[0]) >= 1: |
---|
55 | self.data = treemap_data[0] |
---|
56 | self.addNodes(treemap_data[0]) |
---|
57 | elif type(treemap_data[0]) is Treemap: |
---|
58 | self.data = treemap_data[0] |
---|
59 | return True |
---|
60 | |
---|
61 | if leaf(self, treemap_data): |
---|
62 | return |
---|
63 | |
---|
64 | half = self.getHalfSize(treemap_data) |
---|
65 | treemap1 = treemap_data[0:half] |
---|
66 | treemap2 = treemap_data[half:] |
---|
67 | weight1 = self.getWeight(treemap1) |
---|
68 | weight2 = self.getWeight(treemap2) |
---|
69 | |
---|
70 | self.left = TreemapBTree(data=Treemap(size=weight1)) |
---|
71 | self.right = TreemapBTree(data=Treemap(size=weight2)) |
---|
72 | |
---|
73 | self.left.addNodes(treemap1) |
---|
74 | self.right.addNodes(treemap2) |
---|
75 | |
---|
76 | class TreemapControl(BrowserView): |
---|
77 | """ Class dedicated to control Treemap objects """ |
---|
78 | def __init__(self, context, request, treemap, data): |
---|
79 | super(BrowserView, self).__init__(context, request) |
---|
80 | |
---|
81 | # eg: self.data = SizeByPath(context, request) |
---|
82 | self.data = data |
---|
83 | self.treemap = treemap |
---|
84 | self.binary_tree = TreemapBTree(data=self.treemap) |
---|
85 | |
---|
86 | def setFieldColor(self, data): |
---|
87 | """ |
---|
88 | >>> x = TreemapControl() |
---|
89 | >>> x.setFieldColor([{'id': 'admin'}, {'id': 'admin2'}]) |
---|
90 | [{'color': '#fde5be', 'id': 'admin'}, {'color': '#4af6a0', 'id': 'admin2'}] |
---|
91 | """ |
---|
92 | colors_list = COLOR_FIELD[0:len(data)] |
---|
93 | for i,x in enumerate(data): |
---|
94 | x['color'] = colors_list[i] |
---|
95 | return data |
---|
96 | |
---|
97 | def sortItems(self, data): |
---|
98 | """ Method sorts fields for treemap |
---|
99 | >>> TreemapControl().sortItems([{'id': 'admin', 'size': 10}, {'id': 'admin2', 'size': 20}]) |
---|
100 | [{'id': 'admin2', 'size': 20}, {'id': 'admin', 'size': 10}] |
---|
101 | """ |
---|
102 | getWeight = lambda x: x.get('size') |
---|
103 | return sorted(data, key=getWeight, reverse=True) |
---|
104 | |
---|
105 | def createRectangles(self, data): |
---|
106 | """ Method create treemap rectangles """ |
---|
107 | for field_data in self.sortItems(data): |
---|
108 | self.treemap.addItem(Treemap(size=field_data.get('size'), |
---|
109 | color=field_data.get('color'), |
---|
110 | title=field_data.get('id'))) |
---|
111 | def setTreemapData(self): |
---|
112 | """ Method sets treemap attributes """ |
---|
113 | # self.data.getTreemapInfo() -> [{'id': 'admin', size: '10', 'type': 'Image'},...] |
---|
114 | data = self.data.getTreemapInfo() |
---|
115 | self.setFieldColor(data) |
---|
116 | self.createRectangles(data) |
---|
117 | |
---|
118 | class Treemap(list): |
---|
119 | """ Class dedicated to create rectangle """ |
---|
120 | def __init__(self, x=None, y=None, w=None, h=None, color=None, title="", size=0): |
---|
121 | self.x = x |
---|
122 | self.y = y |
---|
123 | self.w = w # width |
---|
124 | self.h = h # hight |
---|
125 | self.color = color |
---|
126 | self.size = float( size ) |
---|
127 | self.title = title |
---|
128 | |
---|
129 | def coordinates_get(self): |
---|
130 | """ Method gets rectangle coordinates """ |
---|
131 | return (self.x, self.y, self.w, self.h) |
---|
132 | def coordinates_set(self, value): |
---|
133 | """ Method sets rectangle coordinates """ |
---|
134 | (self.x, self.y, self.w, self.h) = value |
---|
135 | coordinates = property(fget=coordinates_get, fset=coordinates_set) |
---|
136 | |
---|
137 | def addItem(self, item): |
---|
138 | """ Method adds treemap object into root treemap """ |
---|
139 | if float(item.size) != 0.0: |
---|
140 | self.append(item) |
---|
141 | |
---|
142 | def addItems(self, items): |
---|
143 | """ Method adds treemap objects into root treemap """ |
---|
144 | for item in items: |
---|
145 | self.addItem(item) |
---|
146 | |
---|
147 | def setCoordinates(self, treemap): |
---|
148 | """ Recursive method adds coordinates into rectangles """ |
---|
149 | if not(treemap.left and treemap.right): |
---|
150 | return |
---|
151 | |
---|
152 | if treemap.data.w > treemap.data.h: |
---|
153 | treemap.left.data.coordinates = (treemap.data.x, treemap.data.y, |
---|
154 | treemap.data.w * (treemap.left.data.size/ |
---|
155 | (treemap.left.data.size + treemap.right.data.size)), |
---|
156 | treemap.data.h) |
---|
157 | treemap.right.data.coordinates = (treemap.data.x + treemap.left.data.w, |
---|
158 | treemap.data.y, |
---|
159 | treemap.data.w - treemap.left.data.w, |
---|
160 | treemap.data.h) |
---|
161 | else: |
---|
162 | treemap.left.data.coordinates = (treemap.data.x, treemap.data.y, |
---|
163 | treemap.data.w, |
---|
164 | treemap.data.h * (treemap.left.data.size/ |
---|
165 | (treemap.left.data.size + treemap.right.data.size))) |
---|
166 | treemap.right.data.coordinates = (treemap.data.x, treemap.data.y + treemap.left.data.h, |
---|
167 | treemap.data.w, |
---|
168 | treemap.data.h - treemap.left.data.h) |
---|
169 | |
---|
170 | self.setCoordinates(treemap.left) |
---|
171 | self.setCoordinates(treemap.right) |
---|
172 | |
---|
173 | |
---|
174 | |
---|
175 | |
---|
176 | class TreemapHtmlControl(TreemapControl): |
---|
177 | """ Class dedicated to control Treemap objects """ |
---|
178 | def __init__(self, context, request): |
---|
179 | data = SizeByPath(context, request) |
---|
180 | super(TreemapHtmlControl, self).__init__(context, request, |
---|
181 | TreemapHtml(), |
---|
182 | data) |
---|
183 | |
---|
184 | def getTreemap(self): |
---|
185 | """ Method generates treemap (using html) """ |
---|
186 | self.setTreemapData() |
---|
187 | self.binary_tree.addNodes(self.treemap) |
---|
188 | self.treemap.setTreemapData(self.binary_tree) |
---|
189 | return self.treemap.createTreemap(self.binary_tree) |
---|
190 | |
---|
191 | class TreemapImageControl(TreemapControl): |
---|
192 | """ Class dedicated to control Treemap objects """ |
---|
193 | def __init__(self, context, request): |
---|
194 | super(TreemapImageControl, self).__init__(context, request, |
---|
195 | TreemapImage(), |
---|
196 | SizeByPath(context, request)) |
---|
197 | def getTreemap(self): |
---|
198 | """ Method generates treemap view (PIL)""" |
---|
199 | self.setTreemapData() |
---|
200 | self.binary_tree.addNodes(self.treemap) |
---|
201 | self.treemap.setCoordinates(self.binary_tree) |
---|
202 | |
---|
203 | treemap = ImageDraw.Draw(self.treemap.image) |
---|
204 | self.treemap.drawTreemap(self.binary_tree, treemap) |
---|
205 | self.treemap.image = self.treemap.drawContainer() |
---|
206 | self.treemap.image = self.treemap.createFrame() |
---|
207 | self.treemap.image.show() |
---|
208 | |
---|
209 | output_file = StringIO() |
---|
210 | self.treemap.image.save(output_file,"PNG") |
---|
211 | try: |
---|
212 | return output_file.getvalue() |
---|
213 | finally: |
---|
214 | output_file.close() |
---|
215 | |
---|
216 | |
---|
217 | class TreemapHtml(Treemap): |
---|
218 | """ Class dedicated to create Treemap objects """ |
---|
219 | def __init__(self, x = 0, y = 0, w = 100, h = 100, title = "Root", |
---|
220 | position=""): |
---|
221 | Treemap.__init__(self, x, y, w, h, title = title) |
---|
222 | # self.__position = position |
---|
223 | |
---|
224 | # def position_get(self): |
---|
225 | # """ Method gets rectangle position """ |
---|
226 | # return self.__position |
---|
227 | # def position_set(self, value): |
---|
228 | # """ Method sets rectangle position """ |
---|
229 | # if value in ['horizontal', 'vertical']: |
---|
230 | # self.__position = value |
---|
231 | # position = property(fget=position_get, fset=position_set) |
---|
232 | |
---|
233 | def getClasses(self, treemap): |
---|
234 | """ Method provides classes for html tag """ |
---|
235 | if not(treemap.left and treemap.right): |
---|
236 | return "" |
---|
237 | |
---|
238 | elif treemap.data.w >= treemap.data.h: |
---|
239 | return 'horizontal prop%d' % int(round(treemap.left.data.w, -1)) |
---|
240 | else: |
---|
241 | return 'vertical prop%d' % int(round(treemap.left.data.h, -1)) |
---|
242 | |
---|
243 | |
---|
244 | def _writeHtml(self, treemap): |
---|
245 | """ Method generates html tag (<div>) """ |
---|
246 | if not(treemap.left and treemap.right): |
---|
247 | return "" |
---|
248 | |
---|
249 | return '<div class="%s" style="background-color:%s">' % \ |
---|
250 | (self.getClasses(treemap.left), treemap.left.data.color) + \ |
---|
251 | self._writeHtml(treemap.left) + '</div>' + \ |
---|
252 | '<div class="%s" style="background-color:%s">'% \ |
---|
253 | (self.getClasses(treemap.right), treemap.right.data.color) + \ |
---|
254 | self._writeHtml(treemap.right) +'</div>' |
---|
255 | |
---|
256 | def createTreemap(self, treemap): |
---|
257 | """ Method generates html tag (treemap) """ |
---|
258 | return '<div class="%s">' % \ |
---|
259 | self.getClasses(treemap) + \ |
---|
260 | self._writeHtml(treemap) + \ |
---|
261 | '</div>' |
---|
262 | |
---|
263 | |
---|
264 | def setTreemapData(self, treemap): |
---|
265 | """ Recursive method adds coordinates into rectangles """ |
---|
266 | if not(treemap.left and treemap.right): |
---|
267 | return |
---|
268 | if treemap.data.w >= treemap.data.h: |
---|
269 | treemap.left.data.coordinates = (None, None, |
---|
270 | 100 * (treemap.left.data.size/ |
---|
271 | (treemap.left.data.size + treemap.right.data.size)), |
---|
272 | 100) |
---|
273 | treemap.right.data.coordinates = (None, |
---|
274 | None, |
---|
275 | treemap.data.w - treemap.left.data.w, |
---|
276 | 100) |
---|
277 | else: |
---|
278 | treemap.left.data.coordinates = (None, None, |
---|
279 | 100, |
---|
280 | 100 * (treemap.left.data.size/ |
---|
281 | (treemap.left.data.size + treemap.right.data.size))) |
---|
282 | treemap.right.data.coordinates = (None, None, |
---|
283 | 100, |
---|
284 | treemap.data.h - treemap.left.data.h) |
---|
285 | |
---|
286 | self.setTreemapData(treemap.left) |
---|
287 | self.setTreemapData(treemap.right) |
---|
288 | |
---|
289 | class TreemapImage(Treemap): |
---|
290 | """ Class dedicated to create treemap image """ |
---|
291 | def __init__(self, x = 0, y = 0, w = WIDTH, h = HEIGHT, title = "Root"): |
---|
292 | Treemap.__init__(self, x, y, w, h, title = title) |
---|
293 | self.image = Image.new("RGB", (w, h), "white") |
---|
294 | self.size = (w, h) |
---|
295 | |
---|
296 | def drawContainer(self): |
---|
297 | """ Method draws treemap (name of item in the top) """ |
---|
298 | font = self.createFont(FONT_SIZE_FATHER, USER_FONT) |
---|
299 | treemap = Image.new( "RGB", self.size, "white") |
---|
300 | rectangle = ImageDraw.Draw(treemap) |
---|
301 | for main in self: |
---|
302 | rectangle.rectangle((main.x, main.y, main.x + main.w, |
---|
303 | main.h + main.y), |
---|
304 | fill=main.color) |
---|
305 | if font.getsize(main.title)[0] < main.w: |
---|
306 | rectangle.text((main.x, main.y), text = main.title, |
---|
307 | font = font) |
---|
308 | region = self.image.crop((int(round(main.x)) + DIVERSION_CHILD, |
---|
309 | int(round(main.y)) + DIVERSION_CHILD, |
---|
310 | int(round(main.x + main.w) - DIVERSION_CHILD), |
---|
311 | int(round(main.h + main.y)) - DIVERSION_CHILD)) |
---|
312 | try: |
---|
313 | region = region.resize((int(round(main.w - DIVERSION_FATHER * 2)), |
---|
314 | int(round(main.h - DIVERSION_FATHER * 2 - DIVERSION_VERTICAL)))) |
---|
315 | except MemoryError: |
---|
316 | continue |
---|
317 | treemap.paste(region,(int(round( main.x + DIVERSION_FATHER)), |
---|
318 | int(round( main.y + DIVERSION_FATHER + DIVERSION_VERTICAL)))) |
---|
319 | return treemap |
---|
320 | |
---|
321 | def rgbHexToDecimal(self, hex_str): |
---|
322 | """ |
---|
323 | >>> TreemapImage().rgbHexToDecimal('#fde5be') |
---|
324 | (253, 229, 190) |
---|
325 | """ |
---|
326 | hex_str = hex_str[1:] |
---|
327 | R = int(hex_str[0:2], 16) |
---|
328 | G = int(hex_str[2:4], 16) |
---|
329 | B = int(hex_str[4:], 16) |
---|
330 | return R,G,B |
---|
331 | |
---|
332 | def getHlsPIL(self, r, g, b): |
---|
333 | """ |
---|
334 | >>> TreemapImage().getHlsPIL(10,10,20) |
---|
335 | 'hsl(10,10%,20%)' |
---|
336 | """ |
---|
337 | return "hsl(%d,%d%%,%d%%)"%(r, g, b) |
---|
338 | |
---|
339 | def getFontSize(self, field): |
---|
340 | """ Method gets font size for rectangles """ |
---|
341 | # font_size = field.w/DIV_SID if field.h < field.w else field.h / DIV_SIDE |
---|
342 | |
---|
343 | if field.h < field.w: |
---|
344 | font_size = field.w / DIV_SIDE |
---|
345 | else: |
---|
346 | font_size = field.h / DIV_SIDE |
---|
347 | if font_size < FONT_SIZE_CHILDREN_MIN: |
---|
348 | return FONT_SIZE_CHILDREN_MIN |
---|
349 | else: |
---|
350 | return font_size |
---|
351 | |
---|
352 | def createFont(self, font_size, font=''): |
---|
353 | """ Method creates font for treemap """ |
---|
354 | return ImageFont.truetype( FONT_PATH + '/vera/Vera%s.ttf'%font, font_size) |
---|
355 | |
---|
356 | def setMask(self, field): |
---|
357 | """ Method sets mask on rectangle """ |
---|
358 | try: |
---|
359 | image_mask = IMAGE_MASK.resize((field.w - DIVERSION_CHILD, |
---|
360 | field.h - DIVERSION_CHILD)) |
---|
361 | except MemoryError: |
---|
362 | return |
---|
363 | self.image.paste("white", (field.x + DIVERSION_CHILD, |
---|
364 | field.y + DIVERSION_CHILD), |
---|
365 | mask = image_mask) |
---|
366 | |
---|
367 | def getPilColor(self, color): |
---|
368 | """ |
---|
369 | >>> TreemapImage().getPilColor('#fde5be') |
---|
370 | 'hsl(37,144%,56%)' |
---|
371 | """ |
---|
372 | r, g, b = self.rgbHexToDecimal(color) |
---|
373 | h, l, s = colorsys.rgb_to_hls(r / DIV_RGB, g / DIV_RGB, b / DIV_RGB) |
---|
374 | l -= TEXT_LIGHT |
---|
375 | s += TEXT_SATURATION |
---|
376 | return self.getHlsPIL(int(h * MAX_ANGLE), int(s * MAX_PERCENT), |
---|
377 | int(l * MAX_PERCENT)) |
---|
378 | |
---|
379 | def writeText(self, treemap, image): |
---|
380 | """ Method dedicated to write text in treemap rectangles """ |
---|
381 | field_name = treemap.data.title |
---|
382 | font = self.createFont(self.getFontSize(treemap.data)) |
---|
383 | |
---|
384 | # font_size = (126, 47); font_size[0] - width, font_size[1] - height |
---|
385 | font_size = font.getsize(field_name) |
---|
386 | if treemap.data.w - DIVERSION_TEXT_CHILD_H + DIVERSION_TEXT_BORDER > font_size[0] and \ |
---|
387 | font_size[1] < treemap.data.h - DIVERSION_TEXT_CHILD_V + DIVERSION_TEXT_BORDER: |
---|
388 | image.text((treemap.data.x + DIVERSION_TEXT_CHILD_H , |
---|
389 | treemap.data.y + DIVERSION_TEXT_CHILD_V), |
---|
390 | text = field_name, font = font, |
---|
391 | fill = self.getPilColor(treemap.data.color)) |
---|
392 | |
---|
393 | def drawTreemap(self, treemap, image): |
---|
394 | """ Method generates treemap image """ |
---|
395 | if treemap.data.color: |
---|
396 | image.rectangle((treemap.data.x + DIVERSION_CHILD, treemap.data.y + DIVERSION_CHILD, |
---|
397 | treemap.data.x + treemap.data.w - DIVERSION_CHILD, |
---|
398 | treemap.data.h + treemap.data.y - DIVERSION_CHILD), |
---|
399 | fill= treemap.data.color ) |
---|
400 | |
---|
401 | self.setMask(treemap.data) |
---|
402 | self.writeText(treemap, image) |
---|
403 | |
---|
404 | if not(treemap.left and treemap.right): |
---|
405 | return |
---|
406 | |
---|
407 | self.drawTreemap(treemap.left, image) |
---|
408 | self.drawTreemap(treemap.right, image) |
---|
409 | |
---|
410 | def createFrame(self): |
---|
411 | """ Method creates frame for treemap image """ |
---|
412 | treemap = Image.new("RGB", |
---|
413 | (WIDTH + FRAME_DIVERSION,HEIGHT + FRAME_DIVERSION), |
---|
414 | FRAME_COLOR) |
---|
415 | treemap.paste(self.image,(FRAME_DIVERSION/2, FRAME_DIVERSION/2)) |
---|
416 | return treemap |
---|