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.data import sort_categories
49 from timelinelib.canvas.drawing.utils import darken_color
50 from timelinelib.features.experimental.experimentalfeatures import EXTENDED_CONTAINER_HEIGHT
51 from timelinelib.utils import unique_based_on_eq
52
53
54 OUTER_PADDING = 5
55 INNER_PADDING = 3
56 DATA_INDICATOR_SIZE = 10
57 SMALL_FONT_SIZE_PX = 11
58 LARGER_FONT_SIZE_PX = 14
59 Y_RECT_OFFSET = 12
60 Y_TEXT_OFFSET = 18
61 ENCODING = "utf-8"
62
63
64 -def export(path, timeline, scene, view_properties, appearence):
68
69
71
72
73
74 - def __init__(self, timeline, scene, view_properties, appearence, **kwargs):
75 self._timeline = timeline
76 self._scene = scene
77 self._appearence = appearence
78 self._view_properties = view_properties
79 self._svg = Svg(width=scene.width, height=scene.height)
80 self._small_font_style = self._get_small_font_style()
81 self._small_centered_font_style = self._get_small_centered_font_style()
82 self._larger_font_style = self._get_larger_font_style()
83 try:
84 self._shadow_flag = kwargs["shadow"]
85 except KeyError:
86 self._shadow_flag = False
87
89 """
90 write the SVG code into the file with filename path. No
91 checking is done if file/path exists
92 """
93 self._svg.save(path, encoding=ENCODING)
94
96 for element in self._get_elements():
97 self._svg.addElement(element)
98
100 elements = [self._define_shadow_filter(), self._get_bg()]
101 elements.extend(self._get_events())
102 elements.extend(self._get_legend())
103 return elements
104
107
112
114 """
115 Draw background color
116 Draw background Era strips and labels
117 Draw major and minor strips, lines to all event boxes and baseline.
118 Both major and minor strips have divider lines and labels.
119 Draw now line if it is visible
120 """
121 group = G()
122 group.addElement(self._draw_background())
123 for era in self._timeline.get_all_periods():
124 group.addElement(self._draw_era_strip(era))
125 group.addElement(self._draw_era_text(era))
126 for strip in self._scene.minor_strip_data:
127 group.addElement(self._draw_minor_strip_divider_line(strip.end_time))
128 group.addElement(self._draw_minor_strip_label(strip))
129 for strip in self._scene.major_strip_data:
130 group.addElement(self._draw_major_strip_divider_line(strip.end_time))
131 group.addElement(self._draw_major_strip_label(strip))
132 group.addElement(self._draw_divider_line())
133 self._draw_lines_to_non_period_events(group, self._view_properties)
134 if self._now_line_is_visible():
135 group.addElement(self._draw_now_line())
136 return group
137
139 svg_color = self._map_svg_color(self._appearence.get_bg_colour())
140 return ShapeBuilder().createRect(0, 0, self._scene.width, self._scene.height, fill=svg_color)
141
148
149 - def _draw_era_text(self, era):
150 x, y = self._calc_era_text_metrics(era)
151 return self._draw_label(era.get_name(), x, y, self._small_centered_font_style)
152
158
160 period = era.get_time_period()
161 _, width = self._calc_era_strip_metrics(era)
162 x = self._scene.x_pos_for_time(period.start_time) + width // 2
163 y = self._scene.height - OUTER_PADDING
164 return x, y
165
168
170 label = self._scene.minor_strip.label(strip_period.start_time)
171 x = self._calc_x_for_minor_strip_label(strip_period)
172 y = self._calc_y_for_minor_strip_label()
173 return self._draw_label(label, x, y, self._small_font_style)
174
178
181
186
189
191 return ShapeBuilder().createLine(x, 0, x, self._scene.height, strokewidth=0.5, stroke=colour)
192
204
206 return ShapeBuilder().createLine(0, self._scene.divider_y, self._scene.width,
207 self._scene.divider_y, strokewidth=0.5, stroke="grey")
208
215
223
225 return self._draw_vertical_line(self._scene.x_pos_for_now(), "darkred")
226
230
233
235 return self._map_svg_color(self._get_event_color(event))
236
239
245
247 """
248 map (r,g,b) color to svg string
249 """
250 return "#%02X%02X%02X" % color[:3]
251
253 return self._appearence.get_legend_visible() and len(categories) > 0
254
261
263 """
264 Draw legend for the given categories.
265
266 Box in lower right corner
267 Motivation for positioning in right corner:
268 SVG text cannot be centered since the text width cannot be calculated
269 and the first part of each event text is important.
270 ergo: text needs to be left aligned.
271 But then the probability is high that a lot of text is at the left
272 bottom
273 ergo: put the legend to the right.
274
275 +----------+
276 | Name O |
277 | Name O |
278 +----------+
279 """
280 group = G()
281 group.addElement(self._draw_categories_box(len(categories)))
282 cur_y = self._get_categories_box_y(len(categories)) + OUTER_PADDING
283 for cat in categories:
284 color_box, label = self._draw_category(self._get_categories_box_width(),
285 self._get_categories_item_height(),
286 self._get_categories_box_x(), cur_y, cat)
287 group.addElement(color_box)
288 group.addElement(label)
289 cur_y = cur_y + self._get_categories_item_height() + INNER_PADDING
290 return group
291
293 return ShapeBuilder().createRect(self._get_categories_box_x(),
294 self._get_categories_box_y(nbr_of_categories),
295 self._get_categories_box_width(),
296 self._get_categories_box_height(nbr_of_categories),
297 fill='white')
298
300
301 return int(self._scene.width * 0.15)
302
305
308
311
313 return self._scene.height - self._get_categories_box_height(nbr_of_categories) - OUTER_PADDING
314
316 return (self._draw_category_color_box(item_height, x, y, cat),
317 self._draw_category_label(width, item_height, x, y, cat))
318
320 base_color = self._map_svg_color(cat.color)
321 border_color = self._map_svg_color(darken_color(cat.color))
322 return ShapeBuilder().createRect(x + OUTER_PADDING,
323 y, item_height, item_height, fill=base_color,
324 stroke=border_color)
325
332
349
351 boxBorderColor = self._get_event_border_color(event)
352 if event.is_container() and EXTENDED_CONTAINER_HEIGHT.enabled():
353 svg_rect = ShapeBuilder().createRect(rect.X, rect.Y - Y_RECT_OFFSET, rect.GetWidth(),
354 rect.GetHeight() + Y_RECT_OFFSET,
355 stroke=boxBorderColor,
356 fill=self._get_event_box_color(event))
357 else:
358 svg_rect = ShapeBuilder().createRect(rect.X, rect.Y, rect.GetWidth(), rect.GetHeight(),
359 stroke=boxBorderColor, fill=self._get_event_box_color(event))
360 if self._shadow_flag:
361 svg_rect.set_filter("url(#filterShadow)")
362 return svg_rect
363
364 - def _draw_contents_indicator(self, event, rect):
365 """
366 The data contents indicator is a small triangle drawn in the upper
367 right corner of the event rectangle.
368 """
369 corner_x = rect.X + rect.Width
370 points = "%d,%d %d,%d %d,%d" % \
371 (corner_x - DATA_INDICATOR_SIZE, rect.Y,
372 corner_x, rect.Y,
373 corner_x, rect.Y + DATA_INDICATOR_SIZE)
374 color = self._get_box_indicator_color(event)
375 indicator = ShapeBuilder().createPolygon(points, fill=color, stroke=color)
376
377 return indicator
378
379 - def _svg_clipped_text(self, text, rect, style, center_text=False):
380 group = G()
381 group.set_clip_path("url(#%s)" % self._create_clip_path(rect))
382 group.addElement(self._draw_text(text, rect, style, center_text))
383 return group
384
386 path_id, path = self._calc_clip_path(rect)
387 clip = ClipPath()
388 clip.addElement(path)
389 clip.set_id(path_id)
390 self._svg.addElement(self._create_defs(clip))
391 return path_id
392
394 rx, ry, width, height = rect
395 if rx < 0:
396 width += rx
397 rx = 0
398 pathId = "path%d_%d_%d" % (rx, ry, width)
399 p = Path(pathData="M %d %d H %d V %d H %d" %
400 (rx, ry + height, rx + width, ry, rx))
401 return pathId, p
402
403 - def _draw_text(self, my_text, rect, style, center_text=False):
404 my_text = self._encode_text(my_text)
405 x, y = self._calc_text_pos(rect, center_text)
406 label = Text(my_text, x, y)
407 label.set_style(style.getStyle())
408 label.set_lengthAdjust("spacingAndGlyphs")
409 return label
410
411 - def _calc_text_pos(self, rect, center_text=False):
412 rx, ry, width, height = rect
413
414
415 if rx < 0:
416 width += rx
417 x = 0
418 else:
419 x = rx + INNER_PADDING
420 if center_text:
421 x += (width - 2 * INNER_PADDING) // 2
422 y = ry + height - INNER_PADDING
423 return x, y
424
425 - def _text(self, the_text, x, y):
426 encoded_text = self._encode_text(the_text)
427 return Text(encoded_text, x, y)
428
429 - def _encode_text(self, text):
430 return xmlescape(text)
431
433 return self._create_defs(self._get_shadow_filter())
434
436 d = Defs()
437 d.addElement(definition)
438 return d
439
442
445
448
450 style = StyleBuilder()
451 style.setStrokeDashArray(dash_array)
452 style.setFontFamily(fontfamily="Verdana")
453 style.setFontSize("%dpx" % size)
454 style.setTextAnchor(anchor)
455 return style
456
458 filterShadow = Filter(x="-.3", y="-.5", width=1.9, height=1.9)
459 filtBlur = FeGaussianBlur(stdDeviation="4")
460 filtBlur.set_in("SourceAlpha")
461 filtBlur.set_result("out1")
462 filtOffset = FeOffset()
463 filtOffset.set_in("out1")
464 filtOffset.set_dx(4)
465 filtOffset.set_dy(-4)
466 filtOffset.set_result("out2")
467 filtMergeNode1 = FeMergeNode()
468 filtMergeNode1.set_in("out2")
469 filtMergeNode2 = FeMergeNode()
470 filtMergeNode2.set_in("SourceGraphic")
471 filtMerge = FeMerge()
472 filtMerge.addElement(filtMergeNode1)
473 filtMerge.addElement(filtMergeNode2)
474 filterShadow.addElement(filtBlur)
475 filterShadow.addElement(filtOffset)
476 filterShadow.addElement(filtMerge)
477 filterShadow.set_id("filterShadow")
478 return filterShadow
479