1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 from xml.sax.saxutils import escape as xmlescape
20
21 try:
22 from pysvg.filter import FeGaussianBlur
23 from pysvg.filter import FeOffset
24 from pysvg.filter import FeMerge
25 from pysvg.filter import FeMergeNode
26 from pysvg.filter import Filter
27 from pysvg.structure import G
28 from pysvg.structure import Svg
29 from pysvg.structure import Defs
30 from pysvg.shape import Path
31 from pysvg.structure import ClipPath
32 from pysvg.text import Text
33 except ImportError:
34 from pysvg.filter import feGaussianBlur as FeGaussianBlur
35 from pysvg.filter import feOffset as FeOffset
36 from pysvg.filter import feMerge as FeMerge
37 from pysvg.filter import feMergeNode as FeMergeNode
38 from pysvg.filter import filter as Filter
39 from pysvg.structure import g as G
40 from pysvg.structure import svg as Svg
41 from pysvg.structure import defs as Defs
42 from pysvg.shape import path as Path
43 from pysvg.structure import clipPath as ClipPath
44 from pysvg.text import text as Text
45 from pysvg.builders import StyleBuilder
46 from pysvg.builders import ShapeBuilder
47
48 from timelinelib.canvas.drawing.utils import darken_color
49 from timelinelib.canvas.data import sort_categories
50 from timelinelib.features.experimental.experimentalfeatures import EXTENDED_CONTAINER_HEIGHT
51
52
53 OUTER_PADDING = 5
54 INNER_PADDING = 3
55 DATA_INDICATOR_SIZE = 10
56 SMALL_FONT_SIZE_PX = 11
57 LARGER_FONT_SIZE_PX = 14
58 Y_RECT_OFFSET = 12
59 Y_TEXT_OFFSET = 18
60 ENCODING = "utf-8"
61
62
63 -def export(path, timeline, scene, view_properties, appearence):
67
68
70
71
72
73 - def __init__(self, timeline, scene, view_properties, appearence, **kwargs):
74 self._timeline = timeline
75 self._scene = scene
76 self._appearence = appearence
77 self._view_properties = view_properties
78 self._svg = Svg(width=scene.width, height=scene.height)
79 self._small_font_style = self._get_small_font_style()
80 self._small_centered_font_style = self._get_small_centered_font_style()
81 self._larger_font_style = self._get_larger_font_style()
82 try:
83 self._shadow_flag = kwargs["shadow"]
84 except KeyError:
85 self._shadow_flag = False
86
88 """
89 write the SVG code into the file with filename path. No
90 checking is done if file/path exists
91 """
92 self._svg.save(path, encoding=ENCODING)
93
95 for element in self._get_elements():
96 self._svg.addElement(element)
97
99 elements = [self._define_shadow_filter(), self._get_bg()]
100 elements.extend(self._get_events())
101 elements.extend(self._get_legend())
102 return elements
103
106
111
113 """
114 Draw background color
115 Draw background Era strips and labels
116 Draw major and minor strips, lines to all event boxes and baseline.
117 Both major and minor strips have divider lines and labels.
118 Draw now line if it is visible
119 """
120 group = G()
121 group.addElement(self._draw_background())
122 for era in self._timeline.get_all_periods():
123 group.addElement(self._draw_era_strip(era))
124 group.addElement(self._draw_era_text(era))
125 for strip in self._scene.minor_strip_data:
126 group.addElement(self._draw_minor_strip_divider_line(strip.end_time))
127 group.addElement(self._draw_minor_strip_label(strip))
128 for strip in self._scene.major_strip_data:
129 group.addElement(self._draw_major_strip_divider_line(strip.end_time))
130 group.addElement(self._draw_major_strip_label(strip))
131 group.addElement(self._draw_divider_line())
132 self._draw_lines_to_non_period_events(group, self._view_properties)
133 if self._now_line_is_visible():
134 group.addElement(self._draw_now_line())
135 return group
136
138 svg_color = self._map_svg_color(self._appearence.get_bg_colour()[:3])
139 return ShapeBuilder().createRect(0, 0, self._scene.width, self._scene.height, fill=svg_color)
140
147
148 - def _draw_era_text(self, era):
149 x, y = self._calc_era_text_metrics(era)
150 return self._draw_label(era.get_name(), x, y, self._small_centered_font_style)
151
157
159 period = era.get_time_period()
160 _, width = self._calc_era_strip_metrics(era)
161 x = self._scene.x_pos_for_time(period.start_time) + width / 2
162 y = self._scene.height - OUTER_PADDING
163 return x, y
164
167
169 label = self._scene.minor_strip.label(strip_period.start_time)
170 x = self._calc_x_for_minor_strip_label(strip_period)
171 y = self._calc_y_for_minor_strip_label()
172 return self._draw_label(label, x, y, self._small_font_style)
173
177
180
185
188
190 return ShapeBuilder().createLine(x, 0, x, self._scene.height, strokewidth=0.5, stroke=colour)
191
203
205 return ShapeBuilder().createLine(0, self._scene.divider_y, self._scene.width,
206 self._scene.divider_y, strokewidth=0.5, stroke="grey")
207
209 for (event, rect) in self._scene.event_data:
210 if rect.Y < self._scene.divider_y:
211 line, circle = self._draw_line_to_non_period_event(view_properties, event, rect)
212 group.addElement(line)
213 group.addElement(circle)
214
222
224 return self._draw_vertical_line(self._scene.x_pos_for_now(), "darkred")
225
229
232
234 return self._map_svg_color(self._get_event_color(event))
235
238
244
246 """
247 map (r,g,b) color to svg string
248 """
249 return "#%02X%02X%02X" % color
250
252 return self._appearence.get_legend_visible() and len(categories) > 0
253
258
260 """
261 Draw legend for the given categories.
262
263 Box in lower right corner
264 Motivation for positioning in right corner:
265 SVG text cannot be centered since the text width cannot be calculated
266 and the first part of each event text is important.
267 ergo: text needs to be left aligned.
268 But then the probability is high that a lot of text is at the left
269 bottom
270 ergo: put the legend to the right.
271
272 +----------+
273 | Name O |
274 | Name O |
275 +----------+
276 """
277 group = G()
278 group.addElement(self._draw_categories_box(len(categories)))
279 cur_y = self._get_categories_box_y(len(categories)) + OUTER_PADDING
280 for cat in categories:
281 color_box, label = self._draw_category(self._get_categories_box_width(),
282 self._get_categories_item_height(),
283 self._get_categories_box_x(), cur_y, cat)
284 group.addElement(color_box)
285 group.addElement(label)
286 cur_y = cur_y + self._get_categories_item_height() + INNER_PADDING
287 return group
288
290 return ShapeBuilder().createRect(self._get_categories_box_x(),
291 self._get_categories_box_y(nbr_of_categories),
292 self._get_categories_box_width(),
293 self._get_categories_box_height(nbr_of_categories),
294 fill='white')
295
297
298 return int(self._scene.width * 0.15)
299
302
305
308
310 return self._scene.height - self._get_categories_box_height(nbr_of_categories) - OUTER_PADDING
311
313 return (self._draw_category_color_box(item_height, x, y, cat),
314 self._draw_category_label(width, item_height, x, y, cat))
315
317 base_color = self._map_svg_color(cat.color)
318 border_color = self._map_svg_color(darken_color(cat.color))
319 return ShapeBuilder().createRect(x + OUTER_PADDING,
320 y, item_height, item_height, fill=base_color,
321 stroke=border_color)
322
329
346
348 boxBorderColor = self._get_event_border_color(event)
349 if event.is_container() and EXTENDED_CONTAINER_HEIGHT.enabled():
350 svg_rect = ShapeBuilder().createRect(rect.X, rect.Y - Y_RECT_OFFSET, rect.GetWidth(),
351 rect.GetHeight() + Y_RECT_OFFSET,
352 stroke=boxBorderColor,
353 fill=self._get_event_box_color(event))
354 else:
355 svg_rect = ShapeBuilder().createRect(rect.X, rect.Y, rect.GetWidth(), rect.GetHeight(),
356 stroke=boxBorderColor, fill=self._get_event_box_color(event))
357 if self._shadow_flag:
358 svg_rect.set_filter("url(#filterShadow)")
359 return svg_rect
360
361 - def _draw_contents_indicator(self, event, rect):
362 """
363 The data contents indicator is a small triangle drawn in the upper
364 right corner of the event rectangle.
365 """
366 corner_x = rect.X + rect.Width
367 points = "%d,%d %d,%d %d,%d" % \
368 (corner_x - DATA_INDICATOR_SIZE, rect.Y,
369 corner_x, rect.Y,
370 corner_x, rect.Y + DATA_INDICATOR_SIZE)
371 color = self._get_box_indicator_color(event)
372 indicator = ShapeBuilder().createPolygon(points, fill=color, stroke=color)
373
374 return indicator
375
376 - def _svg_clipped_text(self, text, rect, style, center_text=False):
377 group = G()
378 group.set_clip_path("url(#%s)" % self._create_clip_path(rect))
379 group.addElement(self._draw_text(text, rect, style, center_text))
380 return group
381
383 path_id, path = self._calc_clip_path(rect)
384 clip = ClipPath()
385 clip.addElement(path)
386 clip.set_id(path_id)
387 self._svg.addElement(self._create_defs(clip))
388 return path_id
389
391 rx, ry, width, height = rect
392 if rx < 0:
393 width += rx
394 rx = 0
395 pathId = "path%d_%d_%d" % (rx, ry, width)
396 p = Path(pathData="M %d %d H %d V %d H %d" %
397 (rx, ry + height, rx + width, ry, rx))
398 return pathId, p
399
400 - def _draw_text(self, my_text, rect, style, center_text=False):
401 my_text = self._encode_text(my_text)
402 x, y = self._calc_text_pos(rect, center_text)
403 label = Text(my_text, x, y)
404 label.set_style(style.getStyle())
405 label.set_lengthAdjust("spacingAndGlyphs")
406 return label
407
408 - def _calc_text_pos(self, rect, center_text=False):
409 rx, ry, width, height = rect
410
411
412 if rx < 0:
413 width += rx
414 x = 0
415 else:
416 x = rx + INNER_PADDING
417 if center_text:
418 x += (width - 2 * INNER_PADDING) / 2
419 y = ry + height - INNER_PADDING
420 return x, y
421
422 - def _text(self, the_text, x, y):
423 encoded_text = self._encode_text(the_text)
424 return Text(encoded_text, x, y)
425
426 - def _encode_text(self, text):
427 return self._encode_unicode_text(xmlescape(text))
428
429 - def _encode_unicode_text(self, text):
430 if isinstance(text, str):
431 return text.encode(ENCODING)
432 else:
433 return text
434
436 return self._create_defs(self._get_shadow_filter())
437
439 d = Defs()
440 d.addElement(definition)
441 return d
442
445
448
451
453 style = StyleBuilder()
454 style.setStrokeDashArray(dash_array)
455 style.setFontFamily(fontfamily="Verdana")
456 style.setFontSize("%dpx" % size)
457 style.setTextAnchor(anchor)
458 return style
459
461 filterShadow = Filter(x="-.3", y="-.5", width=1.9, height=1.9)
462 filtBlur = FeGaussianBlur(stdDeviation="4")
463 filtBlur.set_in("SourceAlpha")
464 filtBlur.set_result("out1")
465 filtOffset = FeOffset()
466 filtOffset.set_in("out1")
467 filtOffset.set_dx(4)
468 filtOffset.set_dy(-4)
469 filtOffset.set_result("out2")
470 filtMergeNode1 = FeMergeNode()
471 filtMergeNode1.set_in("out2")
472 filtMergeNode2 = FeMergeNode()
473 filtMergeNode2.set_in("SourceGraphic")
474 filtMerge = FeMerge()
475 filtMerge.addElement(filtMergeNode1)
476 filtMerge.addElement(filtMergeNode2)
477 filterShadow.addElement(filtBlur)
478 filterShadow.addElement(filtOffset)
479 filterShadow.addElement(filtMerge)
480 filterShadow.set_id("filterShadow")
481 return filterShadow
482